Files cleanup, added summoner code
This commit is contained in:
290
Assets/AI/_Demon/CrystalShooterAI.cs
Normal file
290
Assets/AI/_Demon/CrystalShooterAI.cs
Normal file
@@ -0,0 +1,290 @@
|
||||
using Lean.Pool;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
public class CrystalShooterAI : MonoBehaviour
|
||||
{
|
||||
[Header("Shooting Configuration")]
|
||||
[Tooltip("Transform point from which projectiles are fired")]
|
||||
public Transform muzzle;
|
||||
|
||||
[Tooltip("Fireball prefab (projectile with its own targeting logic)")]
|
||||
public GameObject fireballPrefab;
|
||||
|
||||
[Tooltip("Base seconds between shots")]
|
||||
public float fireRate = 0.7f;
|
||||
|
||||
[Tooltip("Maximum number of shots before auto-despawn")]
|
||||
public int maxShots = 10;
|
||||
|
||||
[Tooltip("Wait time after last shot before despawn")]
|
||||
public float despawnDelay = 3f;
|
||||
|
||||
[Header("Randomization (Desync)")]
|
||||
[Tooltip("Random initial delay after spawn to desync turrets (seconds)")]
|
||||
public Vector2 initialStaggerRange = new Vector2(0.0f, 0.6f);
|
||||
|
||||
[Tooltip("Per-shot random jitter added to fireRate (seconds). Range x means +/- x.")]
|
||||
public float fireRateJitter = 0.2f;
|
||||
|
||||
[Tooltip("Aim wobble in degrees (0 = disabled). Small value adds natural dispersion.")]
|
||||
public float aimJitterDegrees = 0f;
|
||||
|
||||
[Header("Rotation Configuration")]
|
||||
[Tooltip("Yaw rotation speed in degrees per second")]
|
||||
public float turnSpeed = 120f;
|
||||
|
||||
[Tooltip("Idle spin speed when no target (degrees/s, 0 = disabled)")]
|
||||
public float idleSpinSpeed = 30f;
|
||||
|
||||
[Tooltip("Aiming accuracy in degrees (smaller = stricter)")]
|
||||
public float aimTolerance = 5f;
|
||||
|
||||
[Header("Targeting (Turret-Side Only)")]
|
||||
[Tooltip("Auto-find player on start by tag")]
|
||||
public bool autoFindPlayer = true;
|
||||
|
||||
[Tooltip("Player tag to search for")]
|
||||
public string playerTag = "Player";
|
||||
|
||||
[Tooltip("Max range for allowing shots")]
|
||||
public float maxShootingRange = 50f;
|
||||
|
||||
[Header("Effects")]
|
||||
[Tooltip("Enable or disable muzzle flash & sound effects when firing")]
|
||||
public bool useShootEffects = true;
|
||||
|
||||
[Tooltip("Particle effect at shot (pooled)")]
|
||||
public GameObject muzzleFlashPrefab;
|
||||
|
||||
[Tooltip("Shoot sound (played on AudioSource)")]
|
||||
public AudioClip shootSound;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
[Tooltip("Show gizmos in Scene View")]
|
||||
public bool showGizmos = true;
|
||||
|
||||
private Transform target;
|
||||
private AudioSource audioSource;
|
||||
private Coroutine shootingCoroutine;
|
||||
private bool isActive = false;
|
||||
private int shotsFired = 0;
|
||||
private float lastShotTime = 0f;
|
||||
private Transform crystalTransform;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
crystalTransform = transform;
|
||||
|
||||
audioSource = GetComponent<AudioSource>();
|
||||
if (audioSource == null && shootSound != null)
|
||||
{
|
||||
audioSource = gameObject.AddComponent<AudioSource>();
|
||||
audioSource.playOnAwake = false;
|
||||
audioSource.spatialBlend = 1f;
|
||||
}
|
||||
|
||||
if (muzzle == null)
|
||||
{
|
||||
Transform muzzleChild = crystalTransform.Find("muzzle");
|
||||
muzzle = muzzleChild != null ? muzzleChild : crystalTransform;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (autoFindPlayer && target == null)
|
||||
FindPlayer();
|
||||
|
||||
StartShooting();
|
||||
}
|
||||
|
||||
/// <summary> Update tick: rotate towards target or idle spin. </summary>
|
||||
private void Update()
|
||||
{
|
||||
if (!isActive) return;
|
||||
RotateTowardsTarget();
|
||||
}
|
||||
|
||||
/// <summary> Attempts to find the player by tag (for turret-only aiming). </summary>
|
||||
private void FindPlayer()
|
||||
{
|
||||
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
|
||||
if (player != null) SetTarget(player.transform);
|
||||
}
|
||||
|
||||
/// <summary> Sets the turret's aiming target (does NOT propagate to projectiles). </summary>
|
||||
public void SetTarget(Transform newTarget) => target = newTarget;
|
||||
|
||||
/// <summary> Starts the timed shooting routine (fires until maxShots, then despawns). </summary>
|
||||
public void StartShooting()
|
||||
{
|
||||
if (isActive) return;
|
||||
isActive = true;
|
||||
shotsFired = 0;
|
||||
shootingCoroutine = StartCoroutine(ShootingCoroutine());
|
||||
}
|
||||
|
||||
/// <summary> Stops the shooting routine immediately. </summary>
|
||||
public void StopShooting()
|
||||
{
|
||||
isActive = false;
|
||||
if (shootingCoroutine != null)
|
||||
{
|
||||
StopCoroutine(shootingCoroutine);
|
||||
shootingCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main shooting loop with initial spawn stagger and per-shot jitter.
|
||||
/// </summary>
|
||||
private IEnumerator ShootingCoroutine()
|
||||
{
|
||||
// 1) Initial stagger so multiple crystals don't start at the same frame
|
||||
if (initialStaggerRange.y > 0f)
|
||||
{
|
||||
float stagger = Random.Range(initialStaggerRange.x, initialStaggerRange.y);
|
||||
if (stagger > 0f) yield return new WaitForSeconds(stagger);
|
||||
}
|
||||
|
||||
// 2) Normal loop with CanShoot gate and per-shot jittered waits
|
||||
while (shotsFired < maxShots && isActive)
|
||||
{
|
||||
if (CanShoot())
|
||||
{
|
||||
FireFireball();
|
||||
shotsFired++;
|
||||
lastShotTime = Time.time;
|
||||
}
|
||||
|
||||
float wait = fireRate + (fireRateJitter > 0f ? Random.Range(-fireRateJitter, fireRateJitter) : 0f);
|
||||
// Clamp wait to something safe so it never becomes non-positive
|
||||
if (wait < 0.05f) wait = 0.05f;
|
||||
yield return new WaitForSeconds(wait);
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(despawnDelay);
|
||||
DespawnCrystal();
|
||||
}
|
||||
|
||||
/// <summary> Aiming/range gate for firing. </summary>
|
||||
private bool CanShoot()
|
||||
{
|
||||
if (target == null) return false;
|
||||
|
||||
float distanceToTarget = Vector3.Distance(crystalTransform.position, target.position);
|
||||
if (distanceToTarget > maxShootingRange) return false;
|
||||
|
||||
Vector3 directionToTarget = (target.position - crystalTransform.position).normalized;
|
||||
float angleToTarget = Vector3.Angle(crystalTransform.forward, directionToTarget);
|
||||
|
||||
return angleToTarget <= aimTolerance;
|
||||
}
|
||||
|
||||
/// <summary> Spawns a fireball oriented towards the turret's current aim direction with optional dispersion. </summary>
|
||||
private void FireFireball()
|
||||
{
|
||||
if (fireballPrefab == null || muzzle == null) return;
|
||||
|
||||
Vector3 shootDirection;
|
||||
if (target != null)
|
||||
{
|
||||
Vector3 targetCenter = target.position + Vector3.up * 1f;
|
||||
shootDirection = (targetCenter - muzzle.position).normalized;
|
||||
}
|
||||
else shootDirection = crystalTransform.forward;
|
||||
|
||||
// Apply small aim jitter (random yaw/pitch) to avoid perfect sync volleys
|
||||
if (aimJitterDegrees > 0f)
|
||||
{
|
||||
float yaw = Random.Range(-aimJitterDegrees, aimJitterDegrees);
|
||||
float pitch = Random.Range(-aimJitterDegrees * 0.5f, aimJitterDegrees * 0.5f); // usually less pitch dispersion
|
||||
shootDirection = Quaternion.Euler(pitch, yaw, 0f) * shootDirection;
|
||||
}
|
||||
|
||||
Vector3 spawnPosition = muzzle.position;
|
||||
Quaternion spawnRotation = Quaternion.LookRotation(shootDirection);
|
||||
|
||||
LeanPool.Spawn(fireballPrefab, spawnPosition, spawnRotation);
|
||||
PlayShootEffects();
|
||||
}
|
||||
|
||||
/// <summary> Plays muzzle VFX and shoot SFX (if enabled). </summary>
|
||||
private void PlayShootEffects()
|
||||
{
|
||||
if (!useShootEffects) return;
|
||||
|
||||
if (muzzleFlashPrefab != null && muzzle != null)
|
||||
{
|
||||
GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, muzzle.position, muzzle.rotation);
|
||||
LeanPool.Despawn(flash, 2f);
|
||||
}
|
||||
|
||||
if (audioSource != null && shootSound != null)
|
||||
audioSource.PlayOneShot(shootSound);
|
||||
}
|
||||
|
||||
/// <summary> Smooth yaw rotation towards target; idles by spinning when no target. </summary>
|
||||
private void RotateTowardsTarget()
|
||||
{
|
||||
if (target != null)
|
||||
{
|
||||
Vector3 directionToTarget = target.position - crystalTransform.position;
|
||||
directionToTarget.y = 0f;
|
||||
|
||||
if (directionToTarget != Vector3.zero)
|
||||
{
|
||||
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
|
||||
crystalTransform.rotation = Quaternion.RotateTowards(
|
||||
crystalTransform.rotation,
|
||||
targetRotation,
|
||||
turnSpeed * Time.deltaTime
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (idleSpinSpeed > 0f)
|
||||
{
|
||||
crystalTransform.Rotate(Vector3.up, idleSpinSpeed * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Despawns the turret via Lean Pool. </summary>
|
||||
public void DespawnCrystal()
|
||||
{
|
||||
StopShooting();
|
||||
LeanPool.Despawn(gameObject);
|
||||
}
|
||||
|
||||
/// <summary> Forces immediate despawn (e.g., boss death). </summary>
|
||||
public void ForceDespawn() => DespawnCrystal();
|
||||
|
||||
/// <summary> Returns crystal state information. </summary>
|
||||
public bool IsActive() => isActive;
|
||||
|
||||
public int GetShotsFired() => shotsFired;
|
||||
|
||||
public int GetRemainingShots() => Mathf.Max(0, maxShots - shotsFired);
|
||||
|
||||
public float GetTimeSinceLastShot() => Time.time - lastShotTime;
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, maxShootingRange);
|
||||
|
||||
if (muzzle != null)
|
||||
{
|
||||
Gizmos.color = Color.blue;
|
||||
Gizmos.DrawWireSphere(muzzle.position, 0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/AI/_Demon/CrystalShooterAI.cs.meta
Normal file
2
Assets/AI/_Demon/CrystalShooterAI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 906d915677721914dbe9708f218f574a
|
||||
196
Assets/AI/_Demon/Crystals_red 1.mat
Normal file
196
Assets/AI/_Demon/Crystals_red 1.mat
Normal file
@@ -0,0 +1,196 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-7966782797081402813
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Crystals_red 1
|
||||
m_Shader: {fileID: -6465566751694194690, guid: 00a0b3897399f8b42a61a0333dd40ced,
|
||||
type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords:
|
||||
- BASETEXTYPE_ALBEDO_EMISSIVE
|
||||
- USEDISSOLVE_DONT_USE
|
||||
- USEFRESNEL
|
||||
- _ALPHATEST_ON
|
||||
- _USEDISTANCEFADE
|
||||
m_InvalidKeywords:
|
||||
- _EMISSION
|
||||
- _ENVIRONMENTREFLECTIONS_OFF
|
||||
- _METALLICSPECGLOSSMAP
|
||||
- _NORMALMAP
|
||||
m_LightmapFlags: 2
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 1
|
||||
m_CustomRenderQueue: 2450
|
||||
stringTagMap:
|
||||
RenderType: TransparentCutout
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- BaseTex:
|
||||
m_Texture: {fileID: 2800000, guid: 7905193ef00a1844e9b9e2c14ce9be7f, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- DissolveMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- NOSMap:
|
||||
m_Texture: {fileID: 2800000, guid: ead716f17a0a0f347adaf3ff0a109e6a, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 2800000, guid: c0d899801acbe0845902935b9e0e857e, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 2800000, guid: 5a004b07e91ff744aa9143b4cdd46a2d, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 2800000, guid: 284695d770a91574cb92ebad906dc6f4, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: c0d899801acbe0845902935b9e0e857e, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 2800000, guid: 18c6ef854a1f94045bcd407172b8d0f6, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 2800000, guid: 18c6ef854a1f94045bcd407172b8d0f6, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- AOStrength: 3
|
||||
- AlphaClipThreshold: 0.454
|
||||
- BASETEXTYPE: 0
|
||||
- DissolveNoiseScale: 25
|
||||
- EffectStrenght: 1
|
||||
- FresnelPower: 1.37
|
||||
- Metalness: 0.13
|
||||
- NormalStrength: 1.64
|
||||
- Smoothness: 0
|
||||
- USEDISSOLVE: 0
|
||||
- USEDISSOLVEMASK: 0
|
||||
- USEFRESNEL: 1
|
||||
- Vector1_473704f964214ae2bc68475022d1524b: 0.05
|
||||
- _AlphaClip: 1
|
||||
- _AlphaToMask: 1
|
||||
- _BendEffect: 0
|
||||
- _BendMaxDistance: 1
|
||||
- _BendMaxHeight: 0
|
||||
- _BendMinDistance: 0.2
|
||||
- _BendMinHeight: 1
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 0
|
||||
- _BumpScale: 1
|
||||
- _CastShadows: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _EffectThreshold: 0
|
||||
- _EmissionScaleUI: 1.5
|
||||
- _EnvironmentReflections: 0
|
||||
- _FadeDistance: 0.28
|
||||
- _FarFadeDistance: 500
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0.613
|
||||
- _GlossyReflections: 0
|
||||
- _InverseFadeRange: 1
|
||||
- _InverseFarFadeRange: 0.5
|
||||
- _Metallic: 0.772
|
||||
- _Mode: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.02
|
||||
- _QueueControl: 0
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _Smoothness: 0
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _Surface: 0
|
||||
- _Threshold: 0.184
|
||||
- _USEDISTANCEFADE: 1
|
||||
- _USESCANWAVE: 0
|
||||
- _UVSec: 0
|
||||
- _WaveTrail: 4
|
||||
- _WorkflowMode: 1
|
||||
- _ZTest: 4
|
||||
- _ZWrite: 1
|
||||
- _ZWriteControl: 0
|
||||
m_Colors:
|
||||
- BaseColor: {r: 0.92593974, g: 0.92593974, b: 0.92593974, a: 1}
|
||||
- Color_613d1588816440ec9b17710effb7528b: {r: 0, g: 13.98681, b: 714.8679, a: 0}
|
||||
- EmissiveColor: {r: 0.6886792, g: 0.16098994, b: 0, a: 1.5}
|
||||
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
- _BendVector: {r: 0, g: -1, b: 0, a: 0}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
- _EmissionColor: {r: 29.508846, g: 3.0899315, b: 0, a: 1.5}
|
||||
- _EmissionColorUI: {r: 1, g: 0.10344824, b: 0, a: 1}
|
||||
- _FresnelColor: {r: 1, g: 0.113483936, b: 0, a: 0}
|
||||
- _ScanWaveColor: {r: 0, g: 0.5949242, b: 1, a: 0}
|
||||
- _SpecColor: {r: 0.2, g: 0.2, b: 0.2, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
8
Assets/AI/_Demon/Crystals_red 1.mat.meta
Normal file
8
Assets/AI/_Demon/Crystals_red 1.mat.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c11c6b446cf6c4b4cb5b5cd72263e342
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
196
Assets/AI/_Demon/Crystals_red 2.mat
Normal file
196
Assets/AI/_Demon/Crystals_red 2.mat
Normal file
@@ -0,0 +1,196 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-7966782797081402813
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
version: 10
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Crystals_red 2
|
||||
m_Shader: {fileID: -6465566751694194690, guid: 00a0b3897399f8b42a61a0333dd40ced,
|
||||
type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords:
|
||||
- BASETEXTYPE_ALBEDO_EMISSIVE
|
||||
- USEDISSOLVE_DONT_USE
|
||||
- USEFRESNEL
|
||||
- _ALPHATEST_ON
|
||||
- _USEDISTANCEFADE
|
||||
m_InvalidKeywords:
|
||||
- _EMISSION
|
||||
- _ENVIRONMENTREFLECTIONS_OFF
|
||||
- _METALLICSPECGLOSSMAP
|
||||
- _NORMALMAP
|
||||
m_LightmapFlags: 2
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 1
|
||||
m_CustomRenderQueue: 2450
|
||||
stringTagMap:
|
||||
RenderType: TransparentCutout
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- BaseTex:
|
||||
m_Texture: {fileID: 2800000, guid: 7905193ef00a1844e9b9e2c14ce9be7f, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- DissolveMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- NOSMap:
|
||||
m_Texture: {fileID: 2800000, guid: ead716f17a0a0f347adaf3ff0a109e6a, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 2800000, guid: c0d899801acbe0845902935b9e0e857e, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 2800000, guid: 5a004b07e91ff744aa9143b4cdd46a2d, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 2800000, guid: 284695d770a91574cb92ebad906dc6f4, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: c0d899801acbe0845902935b9e0e857e, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 2800000, guid: 18c6ef854a1f94045bcd407172b8d0f6, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 2800000, guid: 18c6ef854a1f94045bcd407172b8d0f6, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- AOStrength: 3
|
||||
- AlphaClipThreshold: 0.454
|
||||
- BASETEXTYPE: 0
|
||||
- DissolveNoiseScale: 25
|
||||
- EffectStrenght: 1
|
||||
- FresnelPower: 1.37
|
||||
- Metalness: 0.13
|
||||
- NormalStrength: 1.64
|
||||
- Smoothness: 0
|
||||
- USEDISSOLVE: 0
|
||||
- USEDISSOLVEMASK: 0
|
||||
- USEFRESNEL: 1
|
||||
- Vector1_473704f964214ae2bc68475022d1524b: 0.05
|
||||
- _AlphaClip: 1
|
||||
- _AlphaToMask: 1
|
||||
- _BendEffect: 0
|
||||
- _BendMaxDistance: 1
|
||||
- _BendMaxHeight: 0
|
||||
- _BendMinDistance: 0.2
|
||||
- _BendMinHeight: 1
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 0
|
||||
- _BumpScale: 1
|
||||
- _CastShadows: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _EffectThreshold: 0
|
||||
- _EmissionScaleUI: 1.5
|
||||
- _EnvironmentReflections: 0
|
||||
- _FadeDistance: 0.28
|
||||
- _FarFadeDistance: 500
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0.613
|
||||
- _GlossyReflections: 0
|
||||
- _InverseFadeRange: 1
|
||||
- _InverseFarFadeRange: 0.5
|
||||
- _Metallic: 0.772
|
||||
- _Mode: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.02
|
||||
- _QueueControl: 0
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _Smoothness: 0
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _Surface: 0
|
||||
- _Threshold: 0.184
|
||||
- _USEDISTANCEFADE: 1
|
||||
- _USESCANWAVE: 0
|
||||
- _UVSec: 0
|
||||
- _WaveTrail: 4
|
||||
- _WorkflowMode: 1
|
||||
- _ZTest: 4
|
||||
- _ZWrite: 1
|
||||
- _ZWriteControl: 0
|
||||
m_Colors:
|
||||
- BaseColor: {r: 0.92593974, g: 0.92593974, b: 0.92593974, a: 1}
|
||||
- Color_613d1588816440ec9b17710effb7528b: {r: 0, g: 13.98681, b: 714.8679, a: 0}
|
||||
- EmissiveColor: {r: 1, g: 1, b: 1, a: 1.5}
|
||||
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
- _BendVector: {r: 0, g: -1, b: 0, a: 0}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
- _EmissionColor: {r: 29.508846, g: 3.0899315, b: 0, a: 1.5}
|
||||
- _EmissionColorUI: {r: 1, g: 0.10344824, b: 0, a: 1}
|
||||
- _FresnelColor: {r: 1, g: 1, b: 1, a: 0}
|
||||
- _ScanWaveColor: {r: 0, g: 0.5949242, b: 1, a: 0}
|
||||
- _SpecColor: {r: 0.2, g: 0.2, b: 0.2, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
8
Assets/AI/_Demon/Crystals_red 2.mat.meta
Normal file
8
Assets/AI/_Demon/Crystals_red 2.mat.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f94eea72c30ac2e45ab55894421ea48c
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
205
Assets/AI/_Demon/DEC_CheckCooldown.cs
Normal file
205
Assets/AI/_Demon/DEC_CheckCooldown.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Decision node checking cooldown for different boss abilities
|
||||
/// Stores Time.time in FSM timers and checks if required cooldown time has passed
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Check Cooldown")]
|
||||
public class DEC_CheckCooldown : vStateDecision
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Check Cooldown";
|
||||
|
||||
[Header("Cooldown Configuration")]
|
||||
[Tooltip("Unique key for this ability (e.g. 'Shield', 'Turret', 'Meteor')")]
|
||||
public string cooldownKey = "Shield";
|
||||
|
||||
[Tooltip("Cooldown time in seconds")]
|
||||
public float cooldownTime = 10f;
|
||||
|
||||
[Tooltip("Whether ability should be available immediately at fight start")]
|
||||
public bool availableAtStart = true;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
/// <summary>
|
||||
/// Main method checking if ability is available
|
||||
/// </summary>
|
||||
/// <returns>True if cooldown has passed and ability can be used</returns>
|
||||
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (fsmBehaviour == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] No FSM Behaviour for key: {cooldownKey}");
|
||||
return false;
|
||||
}
|
||||
|
||||
string timerKey = "cooldown_" + cooldownKey;
|
||||
|
||||
if (!fsmBehaviour.HasTimer(timerKey))
|
||||
{
|
||||
if (availableAtStart)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - available");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetCooldown(fsmBehaviour, cooldownTime);
|
||||
if (enableDebug) Debug.Log($"[DEC_CheckCooldown] First use for {cooldownKey} - setting cooldown");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
|
||||
float timeSinceLastUse = Time.time - lastUsedTime;
|
||||
|
||||
bool isAvailable = timeSinceLastUse >= cooldownTime;
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
if (isAvailable)
|
||||
{
|
||||
Debug.Log($"[DEC_CheckCooldown] {cooldownKey} available - {timeSinceLastUse:F1}s passed of required {cooldownTime}s");
|
||||
}
|
||||
else
|
||||
{
|
||||
float remainingTime = cooldownTime - timeSinceLastUse;
|
||||
Debug.Log($"[DEC_CheckCooldown] {cooldownKey} on cooldown - {remainingTime:F1}s remaining");
|
||||
}
|
||||
}
|
||||
|
||||
return isAvailable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets cooldown for ability - call this after using ability
|
||||
/// </summary>
|
||||
public void SetCooldown(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
SetCooldown(fsmBehaviour, cooldownTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets cooldown with custom time
|
||||
/// </summary>
|
||||
/// <param name="fsmBehaviour">FSM behaviour reference</param>
|
||||
/// <param name="customCooldownTime">Custom cooldown time</param>
|
||||
public void SetCooldown(vIFSMBehaviourController fsmBehaviour, float customCooldownTime)
|
||||
{
|
||||
if (fsmBehaviour == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot set cooldown - no FSM Behaviour");
|
||||
return;
|
||||
}
|
||||
|
||||
string timerKey = "cooldown_" + cooldownKey;
|
||||
|
||||
fsmBehaviour.SetTimer(timerKey, Time.time);
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
Debug.Log($"[DEC_CheckCooldown] Set cooldown for {cooldownKey}: {customCooldownTime}s");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets cooldown - ability becomes immediately available
|
||||
/// </summary>
|
||||
public void ResetCooldown(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (fsmBehaviour == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning($"[DEC_CheckCooldown] Cannot reset cooldown - no FSM Behaviour");
|
||||
return;
|
||||
}
|
||||
|
||||
string timerKey = "cooldown_" + cooldownKey;
|
||||
|
||||
float pastTime = Time.time - cooldownTime - 1f;
|
||||
fsmBehaviour.SetTimer(timerKey, pastTime);
|
||||
|
||||
if (enableDebug) Debug.Log($"[DEC_CheckCooldown] Reset cooldown for {cooldownKey}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns remaining cooldown time in seconds
|
||||
/// </summary>
|
||||
/// <returns>Remaining cooldown time (0 if available)</returns>
|
||||
public float GetRemainingCooldown(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (fsmBehaviour == null) return 0f;
|
||||
|
||||
string timerKey = "cooldown_" + cooldownKey;
|
||||
|
||||
if (!fsmBehaviour.HasTimer(timerKey))
|
||||
{
|
||||
return availableAtStart ? 0f : cooldownTime;
|
||||
}
|
||||
|
||||
float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
|
||||
float timeSinceLastUse = Time.time - lastUsedTime;
|
||||
|
||||
return Mathf.Max(0f, cooldownTime - timeSinceLastUse);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if ability is available without running main Decision logic
|
||||
/// </summary>
|
||||
/// <returns>True if ability is available</returns>
|
||||
public bool IsAvailable(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
return GetRemainingCooldown(fsmBehaviour) <= 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns cooldown progress percentage (0-1)
|
||||
/// </summary>
|
||||
/// <returns>Progress percentage: 0 = just used, 1 = fully recharged</returns>
|
||||
public float GetCooldownProgress(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (cooldownTime <= 0f) return 1f;
|
||||
|
||||
float remainingTime = GetRemainingCooldown(fsmBehaviour);
|
||||
return 1f - (remainingTime / cooldownTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for setting cooldown from external code (e.g. from StateAction)
|
||||
/// </summary>
|
||||
/// <param name="fsmBehaviour">FSM reference</param>
|
||||
/// <param name="key">Ability key</param>
|
||||
/// <param name="cooldown">Cooldown time</param>
|
||||
public static void SetCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown)
|
||||
{
|
||||
if (fsmBehaviour == null) return;
|
||||
|
||||
string timerKey = "cooldown_" + key;
|
||||
fsmBehaviour.SetTimer(timerKey, Time.time);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for checking cooldown from external code
|
||||
/// </summary>
|
||||
/// <param name="fsmBehaviour">FSM reference</param>
|
||||
/// <param name="key">Ability key</param>
|
||||
/// <param name="cooldown">Cooldown time</param>
|
||||
/// <returns>True if available</returns>
|
||||
public static bool CheckCooldownStatic(vIFSMBehaviourController fsmBehaviour, string key, float cooldown)
|
||||
{
|
||||
if (fsmBehaviour == null) return false;
|
||||
|
||||
string timerKey = "cooldown_" + key;
|
||||
|
||||
if (!fsmBehaviour.HasTimer(timerKey)) return true;
|
||||
|
||||
float lastUsedTime = fsmBehaviour.GetTimer(timerKey);
|
||||
return (Time.time - lastUsedTime) >= cooldown;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/AI/_Demon/DEC_CheckCooldown.cs.meta
Normal file
2
Assets/AI/_Demon/DEC_CheckCooldown.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af82d1d082ce4b9478d420f8ca1e72c2
|
||||
105
Assets/AI/_Demon/DEC_TargetClearSky.cs
Normal file
105
Assets/AI/_Demon/DEC_TargetClearSky.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Decision checking if target has clear sky above (for meteor)
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Clear Sky")]
|
||||
public class DEC_TargetClearSky : vStateDecision
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Target Clear Sky";
|
||||
|
||||
[Header("Sky Check Configuration")]
|
||||
[Tooltip("Check height above target")]
|
||||
public float checkHeight = 25f;
|
||||
|
||||
[Tooltip("Obstacle check radius")]
|
||||
public float checkRadius = 2f;
|
||||
|
||||
[Tooltip("Obstacle layer mask")]
|
||||
public LayerMask obstacleLayerMask = -1;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
[Tooltip("Show gizmos in Scene View")]
|
||||
public bool showGizmos = true;
|
||||
|
||||
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
Transform target = GetTarget(fsmBehaviour);
|
||||
if (target == null)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[DEC_TargetClearSky] No target found");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isClear = IsSkyClear(target.position);
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
Debug.Log($"[DEC_TargetClearSky] Sky above target: {(isClear ? "CLEAR" : "BLOCKED")}");
|
||||
}
|
||||
|
||||
return isClear;
|
||||
}
|
||||
|
||||
private bool IsSkyClear(Vector3 targetPosition)
|
||||
{
|
||||
Vector3 skyCheckPoint = targetPosition + Vector3.up * checkHeight;
|
||||
|
||||
if (Physics.CheckSphere(skyCheckPoint, checkRadius, obstacleLayerMask))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Ray skyRay = new Ray(skyCheckPoint, Vector3.down);
|
||||
RaycastHit[] hits = Physics.RaycastAll(skyRay, checkHeight, obstacleLayerMask);
|
||||
|
||||
foreach (var hit in hits)
|
||||
{
|
||||
if (hit.point.y <= targetPosition.y + 0.5f) continue;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Transform GetTarget(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
// Try through AI controller
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null && aiController.currentTarget != null)
|
||||
return aiController.currentTarget.transform;
|
||||
|
||||
// Fallback - find player
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
return player?.transform;
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos) return;
|
||||
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
if (player == null) return;
|
||||
|
||||
Vector3 targetPos = player.transform.position;
|
||||
Vector3 skyCheckPoint = targetPos + Vector3.up * checkHeight;
|
||||
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(skyCheckPoint, checkRadius);
|
||||
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawLine(targetPos, skyCheckPoint);
|
||||
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(targetPos, 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/AI/_Demon/DEC_TargetClearSky.cs.meta
Normal file
2
Assets/AI/_Demon/DEC_TargetClearSky.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a60b21a6b7b97264b856431f5eb253b1
|
||||
58
Assets/AI/_Demon/DEC_TargetFar.cs
Normal file
58
Assets/AI/_Demon/DEC_TargetFar.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Decision checking if target is far away (for Turret ability)
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Decisions/DemonBoss/Target Far")]
|
||||
public class DEC_TargetFar : vStateDecision
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Target Far";
|
||||
|
||||
[Header("Distance Configuration")]
|
||||
[Tooltip("Minimum distance for target to be considered far")]
|
||||
public float minDistance = 8f;
|
||||
|
||||
[Tooltip("Maximum distance for checking")]
|
||||
public float maxDistance = 30f;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
public override bool Decide(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
Transform target = GetTarget(fsmBehaviour);
|
||||
if (target == null)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[DEC_TargetFar] No target found");
|
||||
return false;
|
||||
}
|
||||
|
||||
float distance = Vector3.Distance(fsmBehaviour.transform.position, target.position);
|
||||
bool isFar = distance >= minDistance && distance <= maxDistance;
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
Debug.Log($"[DEC_TargetFar] Distance to target: {distance:F1}m - {(isFar ? "FAR" : "CLOSE")}");
|
||||
}
|
||||
|
||||
return isFar;
|
||||
}
|
||||
|
||||
private Transform GetTarget(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
// Try through AI controller
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null && aiController.currentTarget != null)
|
||||
return aiController.currentTarget.transform;
|
||||
|
||||
// Fallback - find player
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
return player?.transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/AI/_Demon/DEC_TargetFar.cs.meta
Normal file
2
Assets/AI/_Demon/DEC_TargetFar.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6df4a5087a0930d479908e8416bc8a2a
|
||||
15095
Assets/AI/_Demon/DemonShield.prefab
Normal file
15095
Assets/AI/_Demon/DemonShield.prefab
Normal file
File diff suppressed because it is too large
Load Diff
7
Assets/AI/_Demon/DemonShield.prefab.meta
Normal file
7
Assets/AI/_Demon/DemonShield.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a811a4a039beaf4e8ca078a19bdfd33
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
365
Assets/AI/_Demon/DestroyableTurret.cs
Normal file
365
Assets/AI/_Demon/DestroyableTurret.cs
Normal file
@@ -0,0 +1,365 @@
|
||||
using Invector;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Turret component that allows player to destroy crystal turrets
|
||||
/// Implements Invector interfaces for damage system integration
|
||||
/// Attach to turret prefab along with appropriate collider (non-trigger)
|
||||
/// </summary>
|
||||
public class DestroyableTurret : MonoBehaviour, vIDamageReceiver, vIHealthController
|
||||
{
|
||||
[Header("Health Configuration")]
|
||||
[Tooltip("Maximum health points of the turret")]
|
||||
public int maxHealth = 50;
|
||||
|
||||
[Tooltip("Current health points (read-only)")]
|
||||
[SerializeField] private int currentHealth;
|
||||
|
||||
[Header("Destruction Effects")]
|
||||
[Tooltip("Visual effect played when turret is destroyed")]
|
||||
public GameObject destructionEffect;
|
||||
|
||||
[Tooltip("Sound played when turret is destroyed")]
|
||||
public AudioClip destructionSound;
|
||||
|
||||
[Tooltip("Sound played when turret receives damage")]
|
||||
public AudioClip hitSound;
|
||||
|
||||
[Header("Visual Feedback")]
|
||||
[Tooltip("Material applied when turret is damaged")]
|
||||
public Material damagedMaterial;
|
||||
|
||||
[Tooltip("HP threshold below which damaged material is applied (percentage)")]
|
||||
[Range(0f, 1f)]
|
||||
public float damagedThreshold = 0.5f;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logging")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
// Private fields
|
||||
private AudioSource audioSource;
|
||||
|
||||
private CrystalShooterAI shooterAI;
|
||||
private Renderer turretRenderer;
|
||||
private Material originalMaterial;
|
||||
private bool isDestroyed = false;
|
||||
|
||||
// Invector system events
|
||||
public UnityEngine.Events.UnityEvent OnReceiveDamage { get; set; }
|
||||
|
||||
public UnityEngine.Events.UnityEvent OnDead { get; set; }
|
||||
|
||||
// vIHealthController implementation
|
||||
public int currentHealthRecovery { get; set; }
|
||||
|
||||
public int maxHealthRecovery { get; set; }
|
||||
public bool isDead => currentHealth <= 0;
|
||||
|
||||
public OnReceiveDamage onStartReceiveDamage => throw new System.NotImplementedException();
|
||||
|
||||
public OnReceiveDamage onReceiveDamage => throw new System.NotImplementedException();
|
||||
|
||||
public OnDead onDead => throw new System.NotImplementedException();
|
||||
|
||||
float vIHealthController.currentHealth => throw new System.NotImplementedException();
|
||||
|
||||
public int MaxHealth => throw new System.NotImplementedException();
|
||||
|
||||
bool vIHealthController.isDead { get => isDead; set => throw new System.NotImplementedException(); }
|
||||
|
||||
/// <summary>
|
||||
/// Initialize components and validate setup
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
InitializeComponents();
|
||||
InitializeHealth();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find and configure required components
|
||||
/// </summary>
|
||||
private void InitializeComponents()
|
||||
{
|
||||
// Find components
|
||||
shooterAI = GetComponent<CrystalShooterAI>();
|
||||
turretRenderer = GetComponent<Renderer>();
|
||||
|
||||
// Store original material
|
||||
if (turretRenderer != null)
|
||||
{
|
||||
originalMaterial = turretRenderer.material;
|
||||
}
|
||||
|
||||
// Setup AudioSource
|
||||
audioSource = GetComponent<AudioSource>();
|
||||
if (audioSource == null && (destructionSound != null || hitSound != null))
|
||||
{
|
||||
audioSource = gameObject.AddComponent<AudioSource>();
|
||||
audioSource.playOnAwake = false;
|
||||
audioSource.spatialBlend = 1f; // 3D sound
|
||||
}
|
||||
|
||||
// Validate collider setup
|
||||
Collider col = GetComponent<Collider>();
|
||||
if (col != null && col.isTrigger && enableDebug)
|
||||
{
|
||||
Debug.LogWarning("[DestroyableTurret] Collider should not be trigger for damage system!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup initial health values
|
||||
/// </summary>
|
||||
private void InitializeHealth()
|
||||
{
|
||||
currentHealth = maxHealth;
|
||||
maxHealthRecovery = maxHealth;
|
||||
currentHealthRecovery = 0;
|
||||
|
||||
if (enableDebug) Debug.Log($"[DestroyableTurret] Initialized with {currentHealth}/{maxHealth} HP");
|
||||
}
|
||||
|
||||
#region vIDamageReceiver Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Handle incoming damage from Invector damage system
|
||||
/// </summary>
|
||||
public void TakeDamage(vDamage damage)
|
||||
{
|
||||
if (isDestroyed) return;
|
||||
|
||||
int damageValue = Mathf.RoundToInt(damage.damageValue);
|
||||
TakeDamage(damageValue);
|
||||
|
||||
if (enableDebug)
|
||||
Debug.Log($"[DestroyableTurret] Received {damageValue} damage from {damage.sender?.name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle incoming damage with hit reaction parameter
|
||||
/// </summary>
|
||||
public void TakeDamage(vDamage damage, bool hitReaction)
|
||||
{
|
||||
TakeDamage(damage);
|
||||
}
|
||||
|
||||
#endregion vIDamageReceiver Implementation
|
||||
|
||||
#region vIHealthController Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Apply damage to the turret and handle destruction
|
||||
/// </summary>
|
||||
public void TakeDamage(int damage)
|
||||
{
|
||||
if (isDestroyed || currentHealth <= 0) return;
|
||||
|
||||
currentHealth = Mathf.Max(0, currentHealth - damage);
|
||||
|
||||
if (enableDebug)
|
||||
Debug.Log($"[DestroyableTurret] HP: {currentHealth}/{maxHealth} (-{damage})");
|
||||
|
||||
// Play hit effects
|
||||
PlayHitEffects();
|
||||
UpdateVisualState();
|
||||
|
||||
// Trigger damage event
|
||||
OnReceiveDamage?.Invoke();
|
||||
|
||||
// Check if destroyed
|
||||
if (currentHealth <= 0)
|
||||
{
|
||||
DestroyTurret();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change health by specified amount (positive for healing, negative for damage)
|
||||
/// </summary>
|
||||
public void ChangeHealth(int value)
|
||||
{
|
||||
if (isDestroyed) return;
|
||||
|
||||
if (value < 0)
|
||||
{
|
||||
TakeDamage(-value);
|
||||
}
|
||||
else
|
||||
{
|
||||
currentHealth = Mathf.Min(maxHealth, currentHealth + value);
|
||||
if (enableDebug) Debug.Log($"[DestroyableTurret] Healed by {value}, HP: {currentHealth}/{maxHealth}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modify maximum health value
|
||||
/// </summary>
|
||||
public void ChangeMaxHealth(int value)
|
||||
{
|
||||
maxHealth = Mathf.Max(1, maxHealth + value);
|
||||
currentHealth = Mathf.Min(currentHealth, maxHealth);
|
||||
}
|
||||
|
||||
#endregion vIHealthController Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Play sound and visual effects when receiving damage
|
||||
/// </summary>
|
||||
private void PlayHitEffects()
|
||||
{
|
||||
// Play hit sound
|
||||
if (audioSource != null && hitSound != null)
|
||||
{
|
||||
audioSource.PlayOneShot(hitSound);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update visual appearance based on current health
|
||||
/// </summary>
|
||||
private void UpdateVisualState()
|
||||
{
|
||||
// Change material if turret is heavily damaged
|
||||
if (turretRenderer != null && damagedMaterial != null)
|
||||
{
|
||||
float healthPercent = (float)currentHealth / maxHealth;
|
||||
if (healthPercent <= damagedThreshold && turretRenderer.material != damagedMaterial)
|
||||
{
|
||||
turretRenderer.material = damagedMaterial;
|
||||
if (enableDebug) Debug.Log("[DestroyableTurret] Changed to damaged material");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle turret destruction sequence
|
||||
/// </summary>
|
||||
private void DestroyTurret()
|
||||
{
|
||||
if (isDestroyed) return;
|
||||
isDestroyed = true;
|
||||
|
||||
if (enableDebug) Debug.Log("[DestroyableTurret] Turret has been destroyed!");
|
||||
|
||||
// Stop shooting
|
||||
if (shooterAI != null)
|
||||
{
|
||||
shooterAI.StopShooting();
|
||||
}
|
||||
|
||||
// Trigger death event
|
||||
OnDead?.Invoke();
|
||||
|
||||
// Play destruction effects
|
||||
PlayDestructionEffects();
|
||||
|
||||
// Destroy after brief delay (let effects play)
|
||||
StartCoroutine(DestroyAfterDelay(0.1f));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play visual and audio effects for turret destruction
|
||||
/// </summary>
|
||||
private void PlayDestructionEffects()
|
||||
{
|
||||
// Spawn visual effect
|
||||
if (destructionEffect != null)
|
||||
{
|
||||
GameObject effect = Instantiate(destructionEffect, transform.position, transform.rotation);
|
||||
Destroy(effect, 5f);
|
||||
}
|
||||
|
||||
// Play destruction sound
|
||||
if (audioSource != null && destructionSound != null)
|
||||
{
|
||||
audioSource.PlayOneShot(destructionSound);
|
||||
|
||||
// Keep AudioSource alive to finish playing
|
||||
audioSource.transform.parent = null;
|
||||
Destroy(audioSource.gameObject, destructionSound.length + 1f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine to destroy turret after specified delay
|
||||
/// </summary>
|
||||
private System.Collections.IEnumerator DestroyAfterDelay(float delay)
|
||||
{
|
||||
yield return new UnityEngine.WaitForSeconds(delay);
|
||||
|
||||
// Remove through Lean Pool (if used) or regular Destroy
|
||||
if (gameObject.scene.isLoaded) // Check if object still exists
|
||||
{
|
||||
LeanPool.Despawn(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force immediate turret destruction (e.g., when boss dies)
|
||||
/// </summary>
|
||||
public void ForceDestroy()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[DestroyableTurret] Forced destruction");
|
||||
currentHealth = 0;
|
||||
//isDead = true;
|
||||
DestroyTurret();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns current health as percentage of maximum health
|
||||
/// </summary>
|
||||
public float GetHealthPercentage()
|
||||
{
|
||||
return maxHealth > 0 ? (float)currentHealth / maxHealth : 0f;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Draw turret status visualization in Scene View
|
||||
/// </summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
// Show turret status
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
Gizmos.color = currentHealth > maxHealth * 0.5f ? Color.green :
|
||||
currentHealth > maxHealth * 0.25f ? Color.yellow : Color.red;
|
||||
}
|
||||
else
|
||||
{
|
||||
Gizmos.color = Color.green;
|
||||
}
|
||||
|
||||
Gizmos.DrawWireCube(transform.position + Vector3.up * 2f, Vector3.one * 0.5f);
|
||||
|
||||
// HP text in Scene View
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UnityEditor.Handles.Label(transform.position + Vector3.up * 2.5f, $"HP: {currentHealth}/{maxHealth}");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public void AddHealth(int value)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public void ResetHealth(float health)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public void ResetHealth()
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
2
Assets/AI/_Demon/DestroyableTurret.cs.meta
Normal file
2
Assets/AI/_Demon/DestroyableTurret.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 06579ea47ceeddd42a05f7720c15b5de
|
||||
213
Assets/AI/_Demon/FireBall new.prefab
Normal file
213
Assets/AI/_Demon/FireBall new.prefab
Normal file
@@ -0,0 +1,213 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &1947871717301538
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4209731392266258}
|
||||
- component: {fileID: -5586632368230897359}
|
||||
- component: {fileID: 6785567375430979834}
|
||||
m_Layer: 30
|
||||
m_Name: FireBall new
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &4209731392266258
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1947871717301538}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068}
|
||||
m_LocalPosition: {x: 6.67, y: 1, z: -53.73}
|
||||
m_LocalScale: {x: 0.25, y: 0.25, z: 0.25}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 6802751815913059821}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: -90, y: 0, z: 0}
|
||||
--- !u!136 &-5586632368230897359
|
||||
CapsuleCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1947871717301538}
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_IsTrigger: 1
|
||||
m_ProvidesContacts: 0
|
||||
m_Enabled: 1
|
||||
serializedVersion: 2
|
||||
m_Radius: 1
|
||||
m_Height: 0
|
||||
m_Direction: 1
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &6785567375430979834
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1947871717301538}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 1a863cb5e6092ec4a936c4eb21bb9166, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
targetTag: Player
|
||||
targetHeightOffset: 1
|
||||
speed: 12
|
||||
lockTime: 0.5
|
||||
maxLifeTime: 60
|
||||
arrivalTolerance: 0.25
|
||||
damage: 5
|
||||
knockbackForce: 5
|
||||
enableDebug: 0
|
||||
--- !u!1001 &6802374026672139437
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_Modification:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 4209731392266258}
|
||||
m_Modifications:
|
||||
- target: {fileID: 1363404153839300, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: Effect12
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4179900483688768, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4179900483688768, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
propertyPath: m_LocalPosition.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4179900483688768, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
propertyPath: m_LocalPosition.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4179900483688768, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
propertyPath: m_LocalRotation.w
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4179900483688768, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
propertyPath: m_LocalRotation.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4179900483688768, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
propertyPath: m_LocalRotation.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4179900483688768, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
propertyPath: m_LocalRotation.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4179900483688768, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4179900483688768, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4179900483688768, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 114430605913307632, guid: 6fca1031e82d1264a8f6d8d1d7a7cced,
|
||||
type: 3}
|
||||
propertyPath: Speed
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 114430605913307632, guid: 6fca1031e82d1264a8f6d8d1d7a7cced,
|
||||
type: 3}
|
||||
propertyPath: Distance
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 114430605913307632, guid: 6fca1031e82d1264a8f6d8d1d7a7cced,
|
||||
type: 3}
|
||||
propertyPath: m_Enabled
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 114430605913307632, guid: 6fca1031e82d1264a8f6d8d1d7a7cced,
|
||||
type: 3}
|
||||
propertyPath: CollidesWith.m_Bits
|
||||
value: 256
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 114587680334040632, guid: 6fca1031e82d1264a8f6d8d1d7a7cced,
|
||||
type: 3}
|
||||
propertyPath: m_Enabled
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 199693142139168640, guid: 6fca1031e82d1264a8f6d8d1d7a7cced,
|
||||
type: 3}
|
||||
propertyPath: m_Materials.Array.size
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 199728937019303074, guid: 6fca1031e82d1264a8f6d8d1d7a7cced,
|
||||
type: 3}
|
||||
propertyPath: m_Materials.Array.size
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents:
|
||||
- {fileID: 114430605913307632, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
- {fileID: 114587680334040632, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents:
|
||||
- targetCorrespondingSourceObject: {fileID: 1294706121055886, guid: 6fca1031e82d1264a8f6d8d1d7a7cced,
|
||||
type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 1570972138348746864}
|
||||
m_SourcePrefab: {fileID: 100100000, guid: 6fca1031e82d1264a8f6d8d1d7a7cced, type: 3}
|
||||
--- !u!1 &6801133233205277219 stripped
|
||||
GameObject:
|
||||
m_CorrespondingSourceObject: {fileID: 1294706121055886, guid: 6fca1031e82d1264a8f6d8d1d7a7cced,
|
||||
type: 3}
|
||||
m_PrefabInstance: {fileID: 6802374026672139437}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!114 &1570972138348746864
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6801133233205277219}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Version: 3
|
||||
m_UsePipelineSettings: 1
|
||||
m_AdditionalLightsShadowResolutionTier: 2
|
||||
m_LightLayerMask: 1
|
||||
m_RenderingLayers: 1
|
||||
m_CustomShadowLayers: 0
|
||||
m_ShadowLayerMask: 1
|
||||
m_ShadowRenderingLayers: 1
|
||||
m_LightCookieSize: {x: 1, y: 1}
|
||||
m_LightCookieOffset: {x: 0, y: 0}
|
||||
m_SoftShadowQuality: 0
|
||||
--- !u!4 &6802751815913059821 stripped
|
||||
Transform:
|
||||
m_CorrespondingSourceObject: {fileID: 4179900483688768, guid: 6fca1031e82d1264a8f6d8d1d7a7cced,
|
||||
type: 3}
|
||||
m_PrefabInstance: {fileID: 6802374026672139437}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
8
Assets/AI/_Demon/FireBall new.prefab.meta
Normal file
8
Assets/AI/_Demon/FireBall new.prefab.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10813082aa17ea94f9eb5edcc37ac632
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 100100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
9649
Assets/AI/_Demon/FireBall.prefab
Normal file
9649
Assets/AI/_Demon/FireBall.prefab
Normal file
File diff suppressed because it is too large
Load Diff
8
Assets/AI/_Demon/FireBall.prefab.meta
Normal file
8
Assets/AI/_Demon/FireBall.prefab.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9591667a35466484096c6e63785e136c
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 100100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
314
Assets/AI/_Demon/FireballProjectile.cs
Normal file
314
Assets/AI/_Demon/FireballProjectile.cs
Normal file
@@ -0,0 +1,314 @@
|
||||
using Invector;
|
||||
using Invector.vCharacterController;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
public class FireballProjectile : MonoBehaviour
|
||||
{
|
||||
#region Configuration
|
||||
|
||||
[Header("Targeting")]
|
||||
[Tooltip("Tag of the target to chase while tracking phase is active.")]
|
||||
public string targetTag = "Player";
|
||||
|
||||
[Tooltip("Vertical offset added to the target position (e.g., aim at chest/head instead of feet).")]
|
||||
public float targetHeightOffset = 1.0f;
|
||||
|
||||
[Header("Movement")]
|
||||
[Tooltip("Units per second.")]
|
||||
public float speed = 6f;
|
||||
|
||||
[Tooltip("Seconds to track the player before locking to last known position.")]
|
||||
public float lockTime = 15f;
|
||||
|
||||
[Tooltip("Seconds before the projectile auto-despawns regardless of state.")]
|
||||
public float maxLifeTime = 30f;
|
||||
|
||||
[Tooltip("Distance threshold to consider we arrived at the locked position.")]
|
||||
public float arrivalTolerance = 0.25f;
|
||||
|
||||
[Header("Damage")]
|
||||
[Tooltip("Base damage dealt to the target.")]
|
||||
public int damage = 20;
|
||||
|
||||
[Tooltip("Optional knockback impulse magnitude applied along hit direction.")]
|
||||
public float knockbackForce = 0f;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable verbose debug logs.")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
#endregion Configuration
|
||||
|
||||
#region Runtime State
|
||||
|
||||
private Transform player;
|
||||
private Vector3 lockedTargetPos;
|
||||
private bool isLocked = false;
|
||||
private bool hasDealtDamage = false;
|
||||
private float timer = 0f;
|
||||
|
||||
#endregion Runtime State
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Called when the object becomes enabled (including when spawned from a pool).
|
||||
/// Resets runtime state and acquires the target by tag.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
ResetRuntimeState();
|
||||
AcquireTargetByTag();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame update: handle tracking/lock movement and lifetime/despawn conditions.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
|
||||
// Transition to 'locked to last position' after lockTime
|
||||
if (!isLocked && timer >= lockTime && player != null)
|
||||
{
|
||||
lockedTargetPos = player.position + Vector3.up * targetHeightOffset;
|
||||
isLocked = true;
|
||||
if (enableDebug) Debug.Log($"[Fireball] Locked to last known player position: {lockedTargetPos}");
|
||||
}
|
||||
|
||||
// Determine current aim point
|
||||
Vector3 aimPoint;
|
||||
if (!isLocked && player != null)
|
||||
{
|
||||
// Tracking phase: follow live player position
|
||||
aimPoint = player.position + Vector3.up * targetHeightOffset;
|
||||
}
|
||||
else if (isLocked)
|
||||
{
|
||||
// Locked phase: fly to memorized position
|
||||
aimPoint = lockedTargetPos;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: no player found, keep moving forward
|
||||
aimPoint = transform.position + transform.forward * 1000f;
|
||||
}
|
||||
|
||||
// Move towards aim point
|
||||
Vector3 toTarget = aimPoint - transform.position;
|
||||
Vector3 step = toTarget.normalized * speed * Time.deltaTime;
|
||||
transform.position += step;
|
||||
|
||||
// Face movement direction (optional but nice for VFX)
|
||||
if (step.sqrMagnitude > 0.0001f)
|
||||
transform.forward = step.normalized;
|
||||
|
||||
// If locked and close enough to the locked point → despawn
|
||||
if (isLocked && toTarget.magnitude <= arrivalTolerance)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[Fireball] Arrived at locked position → Despawn");
|
||||
Despawn();
|
||||
return;
|
||||
}
|
||||
|
||||
// Hard cap lifetime
|
||||
if (timer >= maxLifeTime)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[Fireball] Max lifetime reached → Despawn");
|
||||
Despawn();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger hit handler – applies damage when colliding with the intended target tag and then despawns.
|
||||
/// </summary>
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (hasDealtDamage) return; // guard against multiple triggers in a single frame
|
||||
|
||||
if (other.CompareTag(targetTag))
|
||||
{
|
||||
DealDamageToTarget(other);
|
||||
Despawn();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Unity Lifecycle
|
||||
|
||||
#region Target Acquisition
|
||||
|
||||
/// <summary>
|
||||
/// Finds the target by tag (e.g., "Player"). Called on enable and can be retried if needed.
|
||||
/// </summary>
|
||||
private void AcquireTargetByTag()
|
||||
{
|
||||
var playerObj = GameObject.FindGameObjectWithTag(targetTag);
|
||||
player = playerObj ? playerObj.transform : null;
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
if (player != null) Debug.Log($"[Fireball] Target found by tag '{targetTag}'.");
|
||||
else Debug.LogWarning($"[Fireball] No target with tag '{targetTag}' found – moving forward fallback.");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Target Acquisition
|
||||
|
||||
#region Damage Pipeline (Invector-style)
|
||||
|
||||
/// <summary>
|
||||
/// Applies damage using available receivers on the hit collider or its parents:
|
||||
/// vIDamageReceiver → vHealthController → vThirdPersonController (with Beyond variant checks).
|
||||
/// </summary>
|
||||
private void DealDamageToTarget(Collider targetCollider)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[FireballDamage] Dealing {damage} damage to: {targetCollider.name}");
|
||||
|
||||
Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
|
||||
Vector3 hitDirection = GetHitDirection(targetCollider);
|
||||
|
||||
// Build vDamage payload (Invector)
|
||||
vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage))
|
||||
{
|
||||
sender = transform,
|
||||
hitPosition = hitPoint
|
||||
};
|
||||
|
||||
if (knockbackForce > 0f)
|
||||
damageInfo.force = hitDirection * knockbackForce;
|
||||
|
||||
bool damageDealt = false;
|
||||
|
||||
// 1) Try generic vIDamageReceiver (collider or parent)
|
||||
var damageReceiver = targetCollider.GetComponent<vIDamageReceiver>() ??
|
||||
targetCollider.GetComponentInParent<vIDamageReceiver>();
|
||||
|
||||
if (damageReceiver != null && !damageDealt)
|
||||
{
|
||||
damageReceiver.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vIDamageReceiver");
|
||||
}
|
||||
|
||||
// 2) Fallback to vHealthController
|
||||
if (!damageDealt)
|
||||
{
|
||||
var healthController = targetCollider.GetComponent<vHealthController>() ??
|
||||
targetCollider.GetComponentInParent<vHealthController>();
|
||||
|
||||
if (healthController != null)
|
||||
{
|
||||
healthController.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vHealthController");
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Fallback to vThirdPersonController (handle Beyond variant)
|
||||
if (!damageDealt)
|
||||
{
|
||||
var tpc = targetCollider.GetComponent<vThirdPersonController>() ??
|
||||
targetCollider.GetComponentInParent<vThirdPersonController>();
|
||||
|
||||
if (tpc != null)
|
||||
{
|
||||
if (tpc is Beyond.bThirdPersonController beyond)
|
||||
{
|
||||
if (!beyond.GodMode && !beyond.isImmortal)
|
||||
{
|
||||
tpc.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through bThirdPersonController");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Target is immortal / GodMode – no damage dealt");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tpc.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
hasDealtDamage = true;
|
||||
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vThirdPersonController");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!damageDealt && enableDebug)
|
||||
Debug.LogWarning("[FireballDamage] Could not deal damage – no valid damage receiver found!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the closest point on the target collider to this projectile position.
|
||||
/// </summary>
|
||||
private Vector3 GetClosestPointOnCollider(Collider col)
|
||||
{
|
||||
return col.ClosestPoint(transform.position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a reasonable hit direction from projectile to target center (fallbacks to forward).
|
||||
/// </summary>
|
||||
private Vector3 GetHitDirection(Collider col)
|
||||
{
|
||||
Vector3 dir = (col.bounds.center - transform.position).normalized;
|
||||
return dir.sqrMagnitude > 0.0001f ? dir : transform.forward;
|
||||
}
|
||||
|
||||
#endregion Damage Pipeline (Invector-style)
|
||||
|
||||
#region Pooling & Utilities
|
||||
|
||||
/// <summary>
|
||||
/// Returns this projectile to the Lean Pool (safe to call on non-pooled too).
|
||||
/// </summary>
|
||||
private void Despawn()
|
||||
{
|
||||
if (enableDebug) Debug.Log("[Fireball] Despawn via LeanPool");
|
||||
LeanPool.Despawn(gameObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears/initializes runtime variables so pooled instances behave like fresh spawns.
|
||||
/// </summary>
|
||||
private void ResetRuntimeState()
|
||||
{
|
||||
timer = 0f;
|
||||
isLocked = false;
|
||||
hasDealtDamage = false;
|
||||
lockedTargetPos = Vector3.zero;
|
||||
}
|
||||
|
||||
#endregion Pooling & Utilities
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Optional gizmo: shows lock radius and direction for quick debugging.
|
||||
/// </summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(transform.position, 0.25f);
|
||||
|
||||
// Draw a small forward arrow
|
||||
Gizmos.DrawLine(transform.position, transform.position + transform.forward * 1.0f);
|
||||
|
||||
// Arrival tolerance sphere (only meaningful when locked)
|
||||
if (isLocked)
|
||||
{
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(lockedTargetPos, arrivalTolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
2
Assets/AI/_Demon/FireballProjectile.cs.meta
Normal file
2
Assets/AI/_Demon/FireballProjectile.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a863cb5e6092ec4a936c4eb21bb9166
|
||||
8334
Assets/AI/_Demon/GhostDemon.prefab
Normal file
8334
Assets/AI/_Demon/GhostDemon.prefab
Normal file
File diff suppressed because it is too large
Load Diff
7
Assets/AI/_Demon/GhostDemon.prefab.meta
Normal file
7
Assets/AI/_Demon/GhostDemon.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82581d6c5dd5945b89394d83db5f4c8b
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
9784
Assets/AI/_Demon/Meteor.prefab
Normal file
9784
Assets/AI/_Demon/Meteor.prefab
Normal file
File diff suppressed because it is too large
Load Diff
8
Assets/AI/_Demon/Meteor.prefab.meta
Normal file
8
Assets/AI/_Demon/Meteor.prefab.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f99aa3faf46a5f94985344f44aaf21aa
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 100100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
400
Assets/AI/_Demon/MeteorProjectile.cs
Normal file
400
Assets/AI/_Demon/MeteorProjectile.cs
Normal file
@@ -0,0 +1,400 @@
|
||||
using Invector;
|
||||
using Invector.vCharacterController;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Enhanced meteor projectile with fireball-like tracking mechanics
|
||||
/// Can either track player or fly to a locked impact point
|
||||
/// </summary>
|
||||
public class MeteorProjectile : MonoBehaviour
|
||||
{
|
||||
#region Inspector: Targeting
|
||||
|
||||
[Header("Targeting")]
|
||||
[Tooltip("If true, use 'overrideImpactPoint' instead of tracking player")]
|
||||
public bool useOverrideImpactPoint = false;
|
||||
|
||||
[Tooltip("Externally provided locked impact point")]
|
||||
public Vector3 overrideImpactPoint;
|
||||
|
||||
[Tooltip("Tag to find and track if not using override point")]
|
||||
public string targetTag = "Player";
|
||||
|
||||
[Tooltip("Height offset for targeting (aim at chest/head)")]
|
||||
public float targetHeightOffset = 1.0f;
|
||||
|
||||
[Tooltip("If true, raycast to ground from impact point")]
|
||||
public bool snapImpactToGround = true;
|
||||
|
||||
[Tooltip("Layers considered 'ground'")]
|
||||
public LayerMask groundMask = ~0;
|
||||
|
||||
[ReadOnlyInInspector] public Vector3 currentTargetPoint;
|
||||
|
||||
#endregion Inspector: Targeting
|
||||
|
||||
#region Inspector: Flight
|
||||
|
||||
[Header("Flight")]
|
||||
[Tooltip("Movement speed in m/s")]
|
||||
public float speed = 25f;
|
||||
|
||||
[Tooltip("Time to track player before locking to position")]
|
||||
public float trackingTime = 1.5f;
|
||||
|
||||
[Tooltip("Distance threshold to consider arrived")]
|
||||
public float arriveEpsilon = 0.5f;
|
||||
|
||||
[Tooltip("Max lifetime in seconds")]
|
||||
public float maxLifetime = 15f;
|
||||
|
||||
#endregion Inspector: Flight
|
||||
|
||||
#region Inspector: Collision & Damage
|
||||
|
||||
[Header("Collision & Damage")]
|
||||
[Tooltip("Collision detection radius")]
|
||||
public float collisionRadius = 0.8f;
|
||||
|
||||
[Tooltip("Layers that stop the meteor")]
|
||||
public LayerMask stopOnLayers = ~0;
|
||||
|
||||
[Tooltip("Layers that take damage")]
|
||||
public LayerMask damageLayers = ~0;
|
||||
|
||||
[Tooltip("Explosion damage radius")]
|
||||
public float explosionRadius = 4f;
|
||||
|
||||
[Tooltip("Base damage")]
|
||||
public int damage = 35;
|
||||
|
||||
[Tooltip("Knockback force")]
|
||||
public float knockbackForce = 12f;
|
||||
|
||||
#endregion Inspector: Collision & Damage
|
||||
|
||||
#region Inspector: Effects
|
||||
|
||||
[Header("Effects & Events")]
|
||||
public GameObject impactVfxPrefab;
|
||||
|
||||
public UnityEvent onSpawn;
|
||||
public UnityEvent onImpact;
|
||||
|
||||
[Header("Debug")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
#endregion Inspector: Effects
|
||||
|
||||
#region Runtime
|
||||
|
||||
private Transform _player;
|
||||
private Vector3 _lockedTarget;
|
||||
private bool _isLocked = false;
|
||||
private bool _hasImpacted = false;
|
||||
private float _lifetime = 0f;
|
||||
private readonly Collider[] _overlapCache = new Collider[32];
|
||||
|
||||
#endregion Runtime
|
||||
|
||||
#region Unity
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
ResetState();
|
||||
InitializeTargeting();
|
||||
onSpawn?.Invoke();
|
||||
|
||||
if (enableDebug) Debug.Log($"[MeteorProjectile] Spawned at {transform.position}");
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_hasImpacted) return;
|
||||
|
||||
_lifetime += Time.deltaTime;
|
||||
|
||||
// Check lifetime limit
|
||||
if (_lifetime >= maxLifetime)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[MeteorProjectile] Lifetime expired");
|
||||
DoImpact(transform.position);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle tracking to lock transition
|
||||
if (!_isLocked && _lifetime >= trackingTime)
|
||||
{
|
||||
LockTarget();
|
||||
}
|
||||
|
||||
// Update target position and move
|
||||
UpdateTargetPosition();
|
||||
MoveTowardsTarget();
|
||||
CheckCollisions();
|
||||
}
|
||||
|
||||
#endregion Unity
|
||||
|
||||
#region Targeting & Movement
|
||||
|
||||
private void ResetState()
|
||||
{
|
||||
_hasImpacted = false;
|
||||
_isLocked = false;
|
||||
_lifetime = 0f;
|
||||
_lockedTarget = Vector3.zero;
|
||||
}
|
||||
|
||||
private void InitializeTargeting()
|
||||
{
|
||||
if (useOverrideImpactPoint)
|
||||
{
|
||||
_lockedTarget = snapImpactToGround ? SnapToGround(overrideImpactPoint) : overrideImpactPoint;
|
||||
_isLocked = true;
|
||||
currentTargetPoint = _lockedTarget;
|
||||
|
||||
if (enableDebug) Debug.Log($"[MeteorProjectile] Using override target: {_lockedTarget}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find player for tracking
|
||||
var playerGO = GameObject.FindGameObjectWithTag(targetTag);
|
||||
_player = playerGO ? playerGO.transform : null;
|
||||
|
||||
if (enableDebug)
|
||||
{
|
||||
if (_player) Debug.Log($"[MeteorProjectile] Found player: {_player.name}");
|
||||
else Debug.LogWarning($"[MeteorProjectile] No player found with tag: {targetTag}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LockTarget()
|
||||
{
|
||||
if (_isLocked) return;
|
||||
|
||||
if (_player != null)
|
||||
{
|
||||
Vector3 targetPos = _player.position + Vector3.up * targetHeightOffset;
|
||||
_lockedTarget = snapImpactToGround ? SnapToGround(targetPos) : targetPos;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: lock to current forward direction
|
||||
_lockedTarget = transform.position + transform.forward * 50f;
|
||||
}
|
||||
|
||||
_isLocked = true;
|
||||
|
||||
if (enableDebug) Debug.Log($"[MeteorProjectile] Target locked to: {_lockedTarget}");
|
||||
}
|
||||
|
||||
private void UpdateTargetPosition()
|
||||
{
|
||||
if (_isLocked)
|
||||
{
|
||||
currentTargetPoint = _lockedTarget;
|
||||
}
|
||||
else if (_player != null)
|
||||
{
|
||||
currentTargetPoint = _player.position + Vector3.up * targetHeightOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No player, keep going forward
|
||||
currentTargetPoint = transform.position + transform.forward * 100f;
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveTowardsTarget()
|
||||
{
|
||||
Vector3 direction = (currentTargetPoint - transform.position).normalized;
|
||||
Vector3 movement = direction * speed * Time.deltaTime;
|
||||
|
||||
transform.position += movement;
|
||||
|
||||
// Face movement direction
|
||||
if (movement.sqrMagnitude > 0.0001f)
|
||||
{
|
||||
transform.rotation = Quaternion.LookRotation(movement.normalized);
|
||||
}
|
||||
|
||||
// Check if we've arrived (only when locked)
|
||||
if (_isLocked && Vector3.Distance(transform.position, currentTargetPoint) <= arriveEpsilon)
|
||||
{
|
||||
if (enableDebug) Debug.Log("[MeteorProjectile] Arrived at target");
|
||||
DoImpact(transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckCollisions()
|
||||
{
|
||||
// Use OverlapSphere for collision detection
|
||||
int hitCount = Physics.OverlapSphereNonAlloc(transform.position, collisionRadius, _overlapCache, stopOnLayers, QueryTriggerInteraction.Ignore);
|
||||
|
||||
if (hitCount > 0)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[MeteorProjectile] Collision detected with {_overlapCache[0].name}");
|
||||
DoImpact(transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Targeting & Movement
|
||||
|
||||
#region Impact & Damage
|
||||
|
||||
private void DoImpact(Vector3 impactPos)
|
||||
{
|
||||
if (_hasImpacted) return;
|
||||
_hasImpacted = true;
|
||||
|
||||
if (enableDebug) Debug.Log($"[MeteorProjectile] Impact at {impactPos}");
|
||||
|
||||
// Spawn VFX
|
||||
if (impactVfxPrefab != null)
|
||||
{
|
||||
var vfx = LeanPool.Spawn(impactVfxPrefab, impactPos, Quaternion.identity);
|
||||
// Auto-despawn VFX after 5 seconds
|
||||
LeanPool.Despawn(vfx, 5f);
|
||||
}
|
||||
|
||||
onImpact?.Invoke();
|
||||
|
||||
// Deal area damage
|
||||
int damageTargets = Physics.OverlapSphereNonAlloc(impactPos, explosionRadius, _overlapCache, damageLayers, QueryTriggerInteraction.Ignore);
|
||||
|
||||
for (int i = 0; i < damageTargets; i++)
|
||||
{
|
||||
var col = _overlapCache[i];
|
||||
if (col != null)
|
||||
{
|
||||
DealDamageToTarget(col, impactPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Despawn meteor
|
||||
LeanPool.Despawn(gameObject);
|
||||
}
|
||||
|
||||
private void DealDamageToTarget(Collider targetCollider, Vector3 hitPoint)
|
||||
{
|
||||
Vector3 hitDirection = (targetCollider.bounds.center - hitPoint).normalized;
|
||||
if (hitDirection.sqrMagnitude < 0.0001f) hitDirection = Vector3.up;
|
||||
|
||||
vDamage damageInfo = new vDamage(damage)
|
||||
{
|
||||
sender = transform,
|
||||
hitPosition = hitPoint
|
||||
};
|
||||
|
||||
if (knockbackForce > 0f)
|
||||
damageInfo.force = hitDirection * knockbackForce;
|
||||
|
||||
bool damageDealt = false;
|
||||
|
||||
// Try vIDamageReceiver first
|
||||
var receiver = targetCollider.GetComponent<vIDamageReceiver>() ??
|
||||
targetCollider.GetComponentInParent<vIDamageReceiver>();
|
||||
if (receiver != null && !damageDealt)
|
||||
{
|
||||
receiver.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
}
|
||||
|
||||
// Fallback to vHealthController
|
||||
if (!damageDealt)
|
||||
{
|
||||
var hc = targetCollider.GetComponent<vHealthController>() ??
|
||||
targetCollider.GetComponentInParent<vHealthController>();
|
||||
if (hc != null)
|
||||
{
|
||||
hc.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tpc.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply physics force
|
||||
var rb = targetCollider.attachedRigidbody;
|
||||
if (rb != null && knockbackForce > 0f)
|
||||
{
|
||||
rb.AddForce(hitDirection * knockbackForce, ForceMode.Impulse);
|
||||
}
|
||||
|
||||
if (enableDebug && damageDealt)
|
||||
{
|
||||
Debug.Log($"[MeteorProjectile] Dealt {damage} damage to {targetCollider.name}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Impact & Damage
|
||||
|
||||
#region Helpers
|
||||
|
||||
private Vector3 SnapToGround(Vector3 point)
|
||||
{
|
||||
Vector3 rayStart = point + Vector3.up * 10f;
|
||||
if (Physics.Raycast(rayStart, Vector3.down, out RaycastHit hit, 50f, groundMask, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
return hit.point;
|
||||
}
|
||||
return point;
|
||||
}
|
||||
|
||||
#endregion Helpers
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
// Draw collision sphere
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, collisionRadius);
|
||||
|
||||
// Draw explosion radius
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, explosionRadius);
|
||||
|
||||
// Draw target point
|
||||
if (currentTargetPoint != Vector3.zero)
|
||||
{
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(currentTargetPoint, 0.5f);
|
||||
Gizmos.DrawLine(transform.position, currentTargetPoint);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
public sealed class ReadOnlyInInspectorAttribute : PropertyAttribute
|
||||
{ }
|
||||
}
|
||||
2
Assets/AI/_Demon/MeteorProjectile.cs.meta
Normal file
2
Assets/AI/_Demon/MeteorProjectile.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d6ec41ae03923f45a963ca66dbb6c56
|
||||
359
Assets/AI/_Demon/SA_CallMeteor.cs
Normal file
359
Assets/AI/_Demon/SA_CallMeteor.cs
Normal file
@@ -0,0 +1,359 @@
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
using UnityEngine.Playables;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Spawns multiple meteors behind the BOSS and launches them toward the player's position.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Call Meteor")]
|
||||
public class SA_CallMeteor : vStateAction
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Call Meteor";
|
||||
|
||||
[Header("Meteor Setup")]
|
||||
[Tooltip("Prefab with MeteorProjectile component")]
|
||||
public GameObject meteorPrefab;
|
||||
|
||||
[Tooltip("Distance behind the BOSS to spawn meteor (meters)")]
|
||||
public float behindBossDistance = 3f;
|
||||
|
||||
[Tooltip("Height above the BOSS to spawn meteor (meters)")]
|
||||
public float aboveBossHeight = 8f;
|
||||
|
||||
[Header("Multi-Meteor Configuration")]
|
||||
[Tooltip("Number of meteors to spawn in sequence")]
|
||||
public int meteorCount = 5;
|
||||
|
||||
[Tooltip("Delay before first meteor spawns (wind-up)")]
|
||||
public float initialCastDelay = 0.4f;
|
||||
|
||||
[Tooltip("Time between each meteor spawn")]
|
||||
public float meteorSpawnInterval = 0.6f;
|
||||
|
||||
[Header("Muzzle Flash Effect")]
|
||||
[Tooltip("Particle effect prefab for muzzle flash at spawn position")]
|
||||
public GameObject muzzleFlashPrefab;
|
||||
|
||||
[Tooltip("Duration to keep muzzle flash alive (seconds)")]
|
||||
public float muzzleFlashDuration = 1.5f;
|
||||
|
||||
[Header("Targeting")]
|
||||
[Tooltip("Tag used to find the target (usually Player)")]
|
||||
public string targetTag = "Player";
|
||||
|
||||
[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 Transform _boss;
|
||||
private Transform _target;
|
||||
|
||||
// --- Multi-meteor state ---
|
||||
private int _meteorsSpawned = 0;
|
||||
|
||||
private bool _spawningActive = false;
|
||||
|
||||
// --- 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)
|
||||
{
|
||||
// Keep the state active until all meteors are spawned
|
||||
if (_spawningActive && _meteorsSpawned < meteorCount)
|
||||
{
|
||||
// Don't allow the state to exit while spawning
|
||||
return;
|
||||
}
|
||||
|
||||
if (_overlayPlaying && Time.time >= _overlayStopAtTime)
|
||||
{
|
||||
StopOverlayWithFade();
|
||||
}
|
||||
|
||||
// Only signal completion when all meteors are done AND overlay is finished
|
||||
if (!_spawningActive && !_overlayPlaying && _meteorsSpawned >= meteorCount)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Sequence complete, ready to exit state");
|
||||
}
|
||||
}
|
||||
else if (execType == vFSMComponentExecutionType.OnStateExit)
|
||||
{
|
||||
OnExit(fsm);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnter(vIFSMBehaviourController fsm)
|
||||
{
|
||||
_boss = fsm.transform;
|
||||
_target = GameObject.FindGameObjectWithTag(targetTag)?.transform;
|
||||
|
||||
if (_target == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No target found – abort");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset multi-meteor state
|
||||
_meteorsSpawned = 0;
|
||||
_spawningActive = true;
|
||||
|
||||
// SET COOLDOWN IMMEDIATELY when meteor ability is used
|
||||
DEC_CheckCooldown.SetCooldownStatic(fsm, "Meteor", 80f);
|
||||
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Boss: {_boss.name}, Target: {_target.name}, Count: {meteorCount}");
|
||||
|
||||
// Fire overlay clip (no Animator params)
|
||||
PlayOverlayOnce(_boss);
|
||||
|
||||
// Start the meteor sequence
|
||||
if (initialCastDelay > 0f)
|
||||
_boss.gameObject.AddComponent<DelayedInvoker>().Init(initialCastDelay, StartMeteorSequence);
|
||||
else
|
||||
StartMeteorSequence();
|
||||
}
|
||||
|
||||
private void OnExit(vIFSMBehaviourController fsm)
|
||||
{
|
||||
// Stop any active spawning
|
||||
_spawningActive = false;
|
||||
StopOverlayImmediate();
|
||||
|
||||
// Clean up any DelayedInvokers attached to the boss
|
||||
var invokers = _boss?.GetComponents<DelayedInvoker>();
|
||||
if (invokers != null)
|
||||
{
|
||||
foreach (var invoker in invokers)
|
||||
{
|
||||
if (invoker != null) Object.Destroy(invoker);
|
||||
}
|
||||
}
|
||||
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] State exited. Spawned {_meteorsSpawned}/{meteorCount} meteors");
|
||||
}
|
||||
|
||||
private void StartMeteorSequence()
|
||||
{
|
||||
if (!_spawningActive) return;
|
||||
SpawnNextMeteor();
|
||||
}
|
||||
|
||||
private void SpawnNextMeteor()
|
||||
{
|
||||
if (!_spawningActive || _meteorsSpawned >= meteorCount) return;
|
||||
|
||||
if (_boss == null || _target == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing boss or target reference");
|
||||
return;
|
||||
}
|
||||
|
||||
SpawnSingleMeteor();
|
||||
_meteorsSpawned++;
|
||||
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Spawned meteor {_meteorsSpawned}/{meteorCount}");
|
||||
|
||||
// Schedule next meteor if needed
|
||||
if (_meteorsSpawned < meteorCount && _spawningActive)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Scheduling next meteor in {meteorSpawnInterval}s");
|
||||
_boss.gameObject.AddComponent<DelayedInvoker>().Init(meteorSpawnInterval, SpawnNextMeteor);
|
||||
}
|
||||
else
|
||||
{
|
||||
// All meteors spawned
|
||||
_spawningActive = false;
|
||||
if (enableDebug) Debug.Log("[SA_CallMeteor] All meteors spawned, sequence complete");
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnSingleMeteor()
|
||||
{
|
||||
if (meteorPrefab == null)
|
||||
{
|
||||
if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing meteorPrefab");
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate spawn position: behind the BOSS + height
|
||||
Vector3 bossForward = _boss.forward.normalized;
|
||||
Vector3 behindBoss = _boss.position - (bossForward * behindBossDistance);
|
||||
Vector3 spawnPos = behindBoss + Vector3.up * aboveBossHeight;
|
||||
|
||||
// Add slight randomization to spawn position for multiple meteors
|
||||
if (_meteorsSpawned > 0)
|
||||
{
|
||||
Vector3 randomOffset = new Vector3(
|
||||
Random.Range(-1f, 1f),
|
||||
Random.Range(-0.5f, 0.5f),
|
||||
Random.Range(-1f, 1f)
|
||||
);
|
||||
spawnPos += randomOffset;
|
||||
}
|
||||
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Spawning meteor #{_meteorsSpawned + 1} at: {spawnPos}");
|
||||
|
||||
// Spawn muzzle flash effect first
|
||||
SpawnMuzzleFlash(spawnPos);
|
||||
|
||||
// Spawn the meteor
|
||||
var meteorGO = LeanPool.Spawn(meteorPrefab, spawnPos, Quaternion.identity);
|
||||
|
||||
// Configure the projectile to target the player
|
||||
var meteorScript = meteorGO.GetComponent<MeteorProjectile>();
|
||||
if (meteorScript != null)
|
||||
{
|
||||
// Update target position for each meteor (player might be moving)
|
||||
Vector3 targetPos = _target.position;
|
||||
|
||||
// Add slight prediction/leading for moving targets
|
||||
var playerRigidbody = _target.GetComponent<Rigidbody>();
|
||||
if (playerRigidbody != null)
|
||||
{
|
||||
Vector3 playerVelocity = playerRigidbody.linearVelocity;
|
||||
float estimatedFlightTime = 2f; // rough estimate
|
||||
targetPos += playerVelocity * estimatedFlightTime * 0.5f; // partial leading
|
||||
}
|
||||
|
||||
meteorScript.useOverrideImpactPoint = true;
|
||||
meteorScript.overrideImpactPoint = targetPos;
|
||||
meteorScript.snapImpactToGround = true;
|
||||
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor #{_meteorsSpawned + 1} configured to target: {targetPos}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebug) Debug.LogError("[SA_CallMeteor] Meteor prefab missing MeteorProjectile component!");
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnMuzzleFlash(Vector3 position)
|
||||
{
|
||||
if (muzzleFlashPrefab == null) return;
|
||||
|
||||
var muzzleFlash = LeanPool.Spawn(muzzleFlashPrefab, position, Quaternion.identity);
|
||||
|
||||
if (muzzleFlashDuration > 0f)
|
||||
{
|
||||
// Auto-despawn muzzle flash after duration
|
||||
_boss.gameObject.AddComponent<DelayedInvoker>().Init(muzzleFlashDuration, () =>
|
||||
{
|
||||
if (muzzleFlash != null)
|
||||
{
|
||||
LeanPool.Despawn(muzzleFlash);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Muzzle flash spawned at: {position}");
|
||||
}
|
||||
|
||||
private void PlayOverlayOnce(Transform owner)
|
||||
{
|
||||
if (overlayClip == null) return;
|
||||
|
||||
var animator = owner.GetComponent<Animator>();
|
||||
if (animator == null) return;
|
||||
|
||||
StopOverlayImmediate();
|
||||
|
||||
_overlayGraph = PlayableGraph.Create("ActionOverlay(CallMeteor)");
|
||||
_overlayGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
|
||||
|
||||
_overlayPlayable = AnimationClipPlayable.Create(_overlayGraph, overlayClip);
|
||||
_overlayPlayable.SetApplyFootIK(false);
|
||||
_overlayPlayable.SetApplyPlayableIK(false);
|
||||
_overlayPlayable.SetSpeed(Mathf.Max(0.0001f, overlaySpeed));
|
||||
|
||||
_overlayOutput = AnimationPlayableOutput.Create(_overlayGraph, "AnimOut", animator);
|
||||
_overlayOutput.SetSourcePlayable(_overlayPlayable);
|
||||
|
||||
_overlayOutput.SetWeight(1f);
|
||||
_overlayGraph.Play();
|
||||
_overlayPlaying = true;
|
||||
|
||||
// Calculate total sequence duration for overlay
|
||||
float totalSequenceDuration = initialCastDelay + (meteorCount * meteorSpawnInterval) + 2f; // +2s buffer
|
||||
float overlayDuration = overlayClip.length / Mathf.Max(0.0001f, overlaySpeed);
|
||||
|
||||
// Use the longer of the two durations, ensuring overlay covers entire sequence
|
||||
float finalDuration = Mathf.Max(overlayDuration, totalSequenceDuration);
|
||||
_overlayStopAtTime = Time.time + finalDuration;
|
||||
|
||||
if (enableDebug) Debug.Log($"[SA_CallMeteor] Overlay clip started via Playables, duration: {finalDuration:F1}s (sequence: {totalSequenceDuration:F1}s, clip: {overlayDuration:F1}s)");
|
||||
}
|
||||
|
||||
private void StopOverlayImmediate()
|
||||
{
|
||||
if (_overlayGraph.IsValid())
|
||||
{
|
||||
_overlayGraph.Stop();
|
||||
_overlayGraph.Destroy();
|
||||
}
|
||||
_overlayPlaying = false;
|
||||
}
|
||||
|
||||
private void StopOverlayWithFade()
|
||||
{
|
||||
if (!_overlayPlaying) { StopOverlayImmediate(); return; }
|
||||
if (_overlayOutput.IsOutputNull() == false) _overlayOutput.SetWeight(0f);
|
||||
StopOverlayImmediate();
|
||||
if (enableDebug) Debug.Log("[SA_CallMeteor] Overlay clip stopped");
|
||||
}
|
||||
|
||||
// Public methods for external monitoring
|
||||
public bool IsSequenceActive() => _spawningActive;
|
||||
|
||||
public int GetMeteorsSpawned() => _meteorsSpawned;
|
||||
|
||||
public int GetTotalMeteorCount() => meteorCount;
|
||||
|
||||
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); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/AI/_Demon/SA_CallMeteor.cs.meta
Normal file
2
Assets/AI/_Demon/SA_CallMeteor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6704e78bcf7b30a43a2d06d91e87d694
|
||||
407
Assets/AI/_Demon/SA_CastShield.cs
Normal file
407
Assets/AI/_Demon/SA_CastShield.cs
Normal file
@@ -0,0 +1,407 @@
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
using UnityEngine.Playables;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Magic Shield
|
||||
/// Spawns shield FX, holds for 'shieldDuration', then plays End and cleans up.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Cast Shield (Start/Keep/End)")]
|
||||
public class SA_CastShield : vStateAction
|
||||
{
|
||||
public override string categoryName => "DemonBoss/Magic";
|
||||
public override string defaultName => "Cast Shield";
|
||||
|
||||
[Header("Shield Logic")]
|
||||
[Tooltip("Prefab with magical shield particle effect")]
|
||||
public GameObject shieldFXPrefab;
|
||||
|
||||
[Tooltip("Shield duration in seconds (time spent in Keep phase before End)")]
|
||||
public float shieldDuration = 5f;
|
||||
|
||||
[Tooltip("Animator bool parameter name for blocking state (optional)")]
|
||||
public string animatorBlockingBool = "IsBlocking";
|
||||
|
||||
[Header("Animation Clips (no Animator params)")]
|
||||
[Tooltip("One-shot intro")]
|
||||
public AnimationClip startClip;
|
||||
|
||||
[Tooltip("Looping hold/maintain")]
|
||||
public AnimationClip keepClip;
|
||||
|
||||
[Tooltip("One-shot outro")]
|
||||
public AnimationClip endClip;
|
||||
|
||||
[Header("Playback & Fades")]
|
||||
[Tooltip("Global playback speed for all clips")]
|
||||
public float clipSpeed = 1f;
|
||||
|
||||
[Tooltip("Seconds to crossfade between phases")]
|
||||
public float crossfadeTime = 0.12f;
|
||||
|
||||
[Tooltip("If the state exits early, still play End quickly before full teardown")]
|
||||
public bool playEndOnEarlyExit = true;
|
||||
|
||||
[Header("Debug")]
|
||||
public bool debugLogs = false;
|
||||
|
||||
// --- Runtime (shield/FX) ---
|
||||
private Transform _npc;
|
||||
|
||||
private Animator _anim;
|
||||
private GameObject _spawnedShieldFX;
|
||||
private float _shieldStartTime;
|
||||
private bool _shieldActive;
|
||||
|
||||
// --- Playables graph state ---
|
||||
private PlayableGraph _graph;
|
||||
|
||||
private AnimationPlayableOutput _output;
|
||||
private AnimationMixerPlayable _mixer; // 2-way mixer for crossfades
|
||||
|
||||
private AnimationClipPlayable _pStart;
|
||||
private AnimationClipPlayable _pKeep;
|
||||
private AnimationClipPlayable _pEnd;
|
||||
|
||||
private enum Phase
|
||||
{ None, Start, Keep, End, Done }
|
||||
|
||||
private Phase _phase = Phase.None;
|
||||
|
||||
// crossfade bookkeeping
|
||||
private int _fromInput = -1; // 0 or 1
|
||||
|
||||
private int _toInput = -1; // 0 or 1
|
||||
private float _fadeT0; // Time.time at fade start
|
||||
private float _fadeDur; // seconds
|
||||
private bool _fading;
|
||||
|
||||
// which clip is bound to each input
|
||||
private AnimationClipPlayable _input0;
|
||||
|
||||
private AnimationClipPlayable _input1;
|
||||
|
||||
// timers for auto-advance
|
||||
private float _phaseScheduledEnd = 0f; // absolute Time.time when Start/End should be done
|
||||
|
||||
// Flag to prevent multiple endings
|
||||
private bool _hasEnded = false;
|
||||
|
||||
public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType execType = vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
if (execType == vFSMComponentExecutionType.OnStateEnter) OnEnter(fsm);
|
||||
else if (execType == vFSMComponentExecutionType.OnStateUpdate) OnUpdate(fsm);
|
||||
else if (execType == vFSMComponentExecutionType.OnStateExit) OnExit(fsm);
|
||||
}
|
||||
|
||||
// ------------------ FSM Hooks ------------------
|
||||
|
||||
private void OnEnter(vIFSMBehaviourController fsm)
|
||||
{
|
||||
// Reset all state
|
||||
_hasEnded = false;
|
||||
_phase = Phase.None;
|
||||
_shieldActive = false;
|
||||
|
||||
_npc = fsm.transform;
|
||||
_anim = _npc.GetComponent<Animator>();
|
||||
|
||||
if (_anim != null && !string.IsNullOrEmpty(animatorBlockingBool))
|
||||
_anim.SetBool(animatorBlockingBool, true);
|
||||
|
||||
// SET COOLDOWN IMMEDIATELY when shield is used
|
||||
DEC_CheckCooldown.SetCooldownStatic(fsm, "Shield", 60f);
|
||||
|
||||
SpawnShieldFX();
|
||||
|
||||
_shieldStartTime = Time.time;
|
||||
_shieldActive = true;
|
||||
|
||||
BuildGraphIfNeeded();
|
||||
|
||||
if (startClip != null)
|
||||
{
|
||||
EnterStart();
|
||||
}
|
||||
else if (keepClip != null)
|
||||
{
|
||||
EnterKeep();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (debugLogs) Debug.Log("[SA_CastShield] No Start/Keep clips; waiting to End.");
|
||||
_phase = Phase.Keep; // logical keep (no anim)
|
||||
}
|
||||
|
||||
if (debugLogs) Debug.Log($"[SA_CastShield] Shield started - duration: {shieldDuration}s, cooldown set to 60s");
|
||||
}
|
||||
|
||||
private void OnUpdate(vIFSMBehaviourController fsm)
|
||||
{
|
||||
// Don't process if we're already done
|
||||
if (_hasEnded || _phase == Phase.Done) return;
|
||||
|
||||
// handle crossfade
|
||||
if (_fading)
|
||||
{
|
||||
float u = Mathf.Clamp01((Time.time - _fadeT0) / Mathf.Max(0.0001f, _fadeDur));
|
||||
if (_fromInput >= 0) _mixer.SetInputWeight(_fromInput, 1f - u);
|
||||
if (_toInput >= 0) _mixer.SetInputWeight(_toInput, u);
|
||||
if (u >= 1f) _fading = false;
|
||||
}
|
||||
|
||||
// auto-advance phases based on timers
|
||||
if (_phase == Phase.Start && Time.time >= _phaseScheduledEnd)
|
||||
{
|
||||
if (keepClip != null) CrossfadeToKeep();
|
||||
else BeginEnd(); // no keep; go straight to End window
|
||||
}
|
||||
else if (_phase == Phase.Keep)
|
||||
{
|
||||
// When shield timer is up, begin End
|
||||
if (_shieldActive && (Time.time - _shieldStartTime) >= shieldDuration)
|
||||
{
|
||||
if (debugLogs) Debug.Log("[SA_CastShield] Shield duration expired, beginning end phase");
|
||||
BeginEnd();
|
||||
}
|
||||
}
|
||||
else if (_phase == Phase.End && Time.time >= _phaseScheduledEnd)
|
||||
{
|
||||
// End completed
|
||||
if (debugLogs) Debug.Log("[SA_CastShield] End phase completed, finishing shield");
|
||||
FinishShield();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnExit(vIFSMBehaviourController fsm)
|
||||
{
|
||||
if (_anim != null && !string.IsNullOrEmpty(animatorBlockingBool))
|
||||
_anim.SetBool(animatorBlockingBool, false);
|
||||
|
||||
// If we left early and haven't ended yet, optionally play End briefly
|
||||
if (playEndOnEarlyExit && !_hasEnded && _phase != Phase.End && _phase != Phase.Done && endClip != null)
|
||||
{
|
||||
if (debugLogs) Debug.Log("[SA_CastShield] Early exit: playing End quickly.");
|
||||
// build graph if it was never built (e.g., no Start/Keep)
|
||||
BuildGraphIfNeeded();
|
||||
CrossfadeTo(endClip, out _pEnd, quick: true);
|
||||
_phase = Phase.End;
|
||||
_phaseScheduledEnd = Time.time + Mathf.Min(endClip.length / SafeSpeed(), 0.25f); // quick outro
|
||||
}
|
||||
|
||||
// Always cleanup when exiting
|
||||
FinishShield();
|
||||
|
||||
if (debugLogs) Debug.Log("[SA_CastShield] State exited and cleaned up");
|
||||
}
|
||||
|
||||
// ------------------ Phase Transitions ------------------
|
||||
|
||||
private void EnterStart()
|
||||
{
|
||||
CrossfadeTo(startClip, out _pStart, quick: false);
|
||||
_phase = Phase.Start;
|
||||
_phaseScheduledEnd = Time.time + (startClip.length / SafeSpeed());
|
||||
if (debugLogs) Debug.Log("[SA_CastShield] Start phase.");
|
||||
}
|
||||
|
||||
private void CrossfadeToKeep()
|
||||
{
|
||||
if (keepClip == null)
|
||||
{
|
||||
BeginEnd();
|
||||
return;
|
||||
}
|
||||
CrossfadeTo(keepClip, out _pKeep, quick: false, loop: true);
|
||||
_phase = Phase.Keep;
|
||||
if (debugLogs) Debug.Log("[SA_CastShield] Switched to Keep (loop).");
|
||||
}
|
||||
|
||||
private void EnterKeep()
|
||||
{
|
||||
CrossfadeTo(keepClip, out _pKeep, quick: false, loop: true);
|
||||
_phase = Phase.Keep;
|
||||
if (debugLogs) Debug.Log("[SA_CastShield] Entered Keep directly.");
|
||||
}
|
||||
|
||||
private void BeginEnd()
|
||||
{
|
||||
// Prevent multiple end calls
|
||||
if (_hasEnded) return;
|
||||
|
||||
if (endClip == null)
|
||||
{
|
||||
// No end clip; just finish
|
||||
FinishShield();
|
||||
return;
|
||||
}
|
||||
CrossfadeTo(endClip, out _pEnd, quick: false);
|
||||
_phase = Phase.End;
|
||||
_phaseScheduledEnd = Time.time + (endClip.length / SafeSpeed());
|
||||
if (debugLogs) Debug.Log("[SA_CastShield] End phase.");
|
||||
}
|
||||
|
||||
private void FinishShield()
|
||||
{
|
||||
// Prevent multiple finish calls
|
||||
if (_hasEnded) return;
|
||||
_hasEnded = true;
|
||||
|
||||
_phase = Phase.Done;
|
||||
TeardownGraph();
|
||||
CleanupShieldFX();
|
||||
_shieldActive = false;
|
||||
|
||||
if (debugLogs) Debug.Log("[SA_CastShield] Shield finished and cleaned up");
|
||||
}
|
||||
|
||||
// ------------------ Graph Setup / Crossfade ------------------
|
||||
|
||||
private void BuildGraphIfNeeded()
|
||||
{
|
||||
if (_graph.IsValid()) return;
|
||||
|
||||
if (_anim == null)
|
||||
{
|
||||
_anim = _npc ? _npc.GetComponent<Animator>() : null;
|
||||
if (_anim == null)
|
||||
{
|
||||
if (debugLogs) Debug.LogWarning("[SA_CastShield] No Animator found; animation disabled.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_graph = PlayableGraph.Create("ShieldOverlayGraph");
|
||||
_graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
|
||||
|
||||
_mixer = AnimationMixerPlayable.Create(_graph, 2); // 2-way mixer
|
||||
_mixer.SetInputWeight(0, 0f);
|
||||
_mixer.SetInputWeight(1, 0f);
|
||||
|
||||
_output = AnimationPlayableOutput.Create(_graph, "AnimOut", _anim);
|
||||
_output.SetSourcePlayable(_mixer);
|
||||
_output.SetWeight(1f);
|
||||
|
||||
_graph.Play();
|
||||
}
|
||||
|
||||
private void TeardownGraph()
|
||||
{
|
||||
if (_graph.IsValid())
|
||||
{
|
||||
_graph.Stop();
|
||||
_graph.Destroy();
|
||||
}
|
||||
_input0 = default;
|
||||
_input1 = default;
|
||||
_fromInput = _toInput = -1;
|
||||
_fading = false;
|
||||
}
|
||||
|
||||
private float SafeSpeed() => Mathf.Max(0.0001f, clipSpeed);
|
||||
|
||||
/// <summary>
|
||||
/// Fades from whatever is currently audible to the given clip.
|
||||
/// Reuses the 2 inputs; swaps playables as needed.
|
||||
/// </summary>
|
||||
private void CrossfadeTo(AnimationClip clip,
|
||||
out AnimationClipPlayable playableOut,
|
||||
bool quick,
|
||||
bool loop = false)
|
||||
{
|
||||
playableOut = default;
|
||||
|
||||
if (!_graph.IsValid() || clip == null) return;
|
||||
|
||||
float speed = SafeSpeed();
|
||||
|
||||
// Prepare or reuse a slot (two inputs that we swap between)
|
||||
// Choose the silent input as target; if none is silent, pick the opposite of the currently "from".
|
||||
int targetInput = (_mixer.GetInputWeight(0) < 0.0001f) ? 0 : (_mixer.GetInputWeight(1) < 0.0001f) ? 1 : (_toInput == 0 ? 1 : 0);
|
||||
|
||||
// Destroy existing playable on that input if any
|
||||
var currentPlayableOnTarget = (AnimationClipPlayable)_mixer.GetInput(targetInput);
|
||||
if (currentPlayableOnTarget.IsValid())
|
||||
{
|
||||
_graph.Disconnect(_mixer, targetInput);
|
||||
currentPlayableOnTarget.Destroy();
|
||||
}
|
||||
|
||||
// Create new clip playable
|
||||
var newPlayable = AnimationClipPlayable.Create(_graph, clip);
|
||||
newPlayable.SetApplyFootIK(false);
|
||||
newPlayable.SetApplyPlayableIK(false);
|
||||
newPlayable.SetSpeed(speed);
|
||||
|
||||
// Set looping if requested
|
||||
if (loop)
|
||||
{
|
||||
newPlayable.SetDuration(double.PositiveInfinity);
|
||||
}
|
||||
|
||||
// Connect to mixer
|
||||
_graph.Connect(newPlayable, 0, _mixer, targetInput);
|
||||
_mixer.SetInputWeight(targetInput, 0f);
|
||||
|
||||
// Cache which playable is on which slot for optional debug
|
||||
if (targetInput == 0) _input0 = newPlayable; else _input1 = newPlayable;
|
||||
|
||||
// Determine current audible input to fade from
|
||||
int sourceInput = (targetInput == 0) ? 1 : 0;
|
||||
float wSource = _mixer.GetInputWeight(sourceInput);
|
||||
bool hasSource = wSource > 0.0001f && _mixer.GetInput(sourceInput).IsValid();
|
||||
|
||||
// Start from beginning for new phase
|
||||
newPlayable.SetTime(0);
|
||||
playableOut = newPlayable;
|
||||
|
||||
// Configure fade
|
||||
_fromInput = hasSource ? sourceInput : -1;
|
||||
_toInput = targetInput;
|
||||
_fadeDur = Mathf.Max(0f, quick ? Mathf.Min(0.06f, crossfadeTime) : crossfadeTime);
|
||||
_fadeT0 = Time.time;
|
||||
_fading = _fadeDur > 0f && hasSource;
|
||||
|
||||
// Set immediate weights if not fading
|
||||
if (!_fading)
|
||||
{
|
||||
if (_fromInput >= 0) _mixer.SetInputWeight(_fromInput, 0f);
|
||||
_mixer.SetInputWeight(_toInput, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------ FX Helpers ------------------
|
||||
|
||||
private void SpawnShieldFX()
|
||||
{
|
||||
if (shieldFXPrefab == null) return;
|
||||
_spawnedShieldFX = LeanPool.Spawn(shieldFXPrefab, _npc.position, _npc.rotation);
|
||||
if (debugLogs) Debug.Log("[SA_CastShield] Shield FX spawned.");
|
||||
}
|
||||
|
||||
private void CleanupShieldFX()
|
||||
{
|
||||
if (_spawnedShieldFX != null)
|
||||
{
|
||||
LeanPool.Despawn(_spawnedShieldFX);
|
||||
_spawnedShieldFX = null;
|
||||
if (debugLogs) Debug.Log("[SA_CastShield] Shield FX despawned.");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------ Public Query ------------------
|
||||
|
||||
public bool IsShieldActive() => _shieldActive && !_hasEnded;
|
||||
|
||||
public float GetRemainingShieldTime()
|
||||
{
|
||||
if (!_shieldActive || _hasEnded) return 0f;
|
||||
float t = Mathf.Max(0f, shieldDuration - (Time.time - _shieldStartTime));
|
||||
return (_phase == Phase.End || _phase == Phase.Done) ? 0f : t;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/AI/_Demon/SA_CastShield.cs.meta
Normal file
2
Assets/AI/_Demon/SA_CastShield.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c53af76a8a097a244a3002b8aa7b4ceb
|
||||
349
Assets/AI/_Demon/SA_SpawnTurretSmart.cs
Normal file
349
Assets/AI/_Demon/SA_SpawnTurretSmart.cs
Normal file
@@ -0,0 +1,349 @@
|
||||
using Invector.vCharacterController.AI.FSMBehaviour;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
using UnityEngine.Playables;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Spawns exactly 3 crystal turrets around the opponent.
|
||||
/// Now with per-spawn randomness and position validation.
|
||||
/// </summary>
|
||||
[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 3 Turrets Radial";
|
||||
|
||||
[Header("Turret Configuration")]
|
||||
public GameObject crystalPrefab;
|
||||
|
||||
public float minSpawnDistance = 2f;
|
||||
public float maxSpawnDistance = 6f;
|
||||
public float obstacleCheckRadius = 1f;
|
||||
public float groundCheckHeight = 2f;
|
||||
public LayerMask obstacleLayerMask = -1;
|
||||
public LayerMask groundLayerMask = -1;
|
||||
public string animatorBlockingBool = "IsBlocking";
|
||||
|
||||
[Header("Placement Search (Validation)")]
|
||||
public int perTurretAdjustmentTries = 10;
|
||||
|
||||
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("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;
|
||||
|
||||
public bool showGizmos = true;
|
||||
|
||||
private Animator npcAnimator;
|
||||
private Transform npcTransform;
|
||||
private Transform playerTransform;
|
||||
|
||||
private readonly System.Collections.Generic.List<Vector3> _lastPlanned =
|
||||
new System.Collections.Generic.List<Vector3>(3);
|
||||
|
||||
// --- Playables runtime ---
|
||||
private PlayableGraph _overlayGraph;
|
||||
|
||||
private AnimationPlayableOutput _overlayOutput;
|
||||
private AnimationClipPlayable _overlayPlayable;
|
||||
private bool _overlayPlaying;
|
||||
private float _overlayStopAtTime;
|
||||
|
||||
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
if (executionType == vFSMComponentExecutionType.OnStateEnter) OnStateEnter(fsmBehaviour);
|
||||
else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
|
||||
{
|
||||
// Auto-stop overlay when finished
|
||||
if (_overlayPlaying && Time.time >= _overlayStopAtTime) StopOverlayWithFade();
|
||||
}
|
||||
else if (executionType == vFSMComponentExecutionType.OnStateExit) OnStateExit(fsmBehaviour);
|
||||
}
|
||||
|
||||
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
npcTransform = fsmBehaviour.transform;
|
||||
npcAnimator = npcTransform.GetComponent<Animator>();
|
||||
|
||||
FindPlayer(fsmBehaviour);
|
||||
|
||||
if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
|
||||
npcAnimator.SetBool(animatorBlockingBool, true);
|
||||
|
||||
PlayOverlayOnce(npcTransform);
|
||||
|
||||
SpawnThreeTurretsRadial(fsmBehaviour);
|
||||
|
||||
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f);
|
||||
}
|
||||
|
||||
private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
|
||||
npcAnimator.SetBool(animatorBlockingBool, false);
|
||||
|
||||
StopOverlayWithFade();
|
||||
}
|
||||
|
||||
private void FindPlayer(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
if (player != null)
|
||||
{
|
||||
playerTransform = player.transform;
|
||||
return;
|
||||
}
|
||||
|
||||
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
|
||||
if (aiController != null && aiController.currentTarget != null)
|
||||
playerTransform = aiController.currentTarget.transform;
|
||||
}
|
||||
|
||||
private void SpawnThreeTurretsRadial(vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
if (crystalPrefab == null)
|
||||
{
|
||||
Debug.LogError("[SA_SpawnTurretSmart] Missing crystalPrefab!");
|
||||
return;
|
||||
}
|
||||
|
||||
_lastPlanned.Clear();
|
||||
|
||||
Vector3 center = playerTransform != null ? playerTransform.position : npcTransform.position;
|
||||
float baseRadius = Mathf.Clamp((minSpawnDistance + maxSpawnDistance) * 0.5f, minSpawnDistance, maxSpawnDistance);
|
||||
|
||||
Vector3 refDir = Vector3.forward;
|
||||
if (playerTransform != null && npcTransform != null)
|
||||
{
|
||||
Vector3 d = (npcTransform.position - center); d.y = 0f;
|
||||
if (d.sqrMagnitude > 0.0001f) refDir = d.normalized;
|
||||
}
|
||||
else if (npcTransform != null)
|
||||
{
|
||||
refDir = npcTransform.forward;
|
||||
}
|
||||
|
||||
float globalOffset = globalStartAngleRandom ? Random.Range(0f, 360f) : 0f;
|
||||
|
||||
const int turretCount = 3;
|
||||
for (int i = 0; i < turretCount; i++)
|
||||
{
|
||||
float baseAngle = globalOffset + i * 120f;
|
||||
float angle = baseAngle + (perTurretAngleJitter > 0f ? Random.Range(-perTurretAngleJitter, perTurretAngleJitter) : 0f);
|
||||
|
||||
float radius = baseRadius + (perTurretRadiusJitter > 0f ? Random.Range(-perTurretRadiusJitter, perTurretRadiusJitter) : 0f);
|
||||
radius = Mathf.Clamp(radius, minSpawnDistance, maxSpawnDistance);
|
||||
|
||||
Vector3 ideal = center + Quaternion.Euler(0f, angle, 0f) * refDir * radius;
|
||||
|
||||
Vector3? chosen = IsPositionValidAndSeparated(ideal)
|
||||
? (Vector3?)ideal
|
||||
: FindValidPositionAroundSpoke(center, refDir, angle, radius);
|
||||
|
||||
if (!chosen.HasValue)
|
||||
{
|
||||
Vector3 rough = center + Quaternion.Euler(0f, angle, 0f) * refDir * radius;
|
||||
chosen = EnforceSeparationFallback(rough, center);
|
||||
}
|
||||
|
||||
Vector3 spawnPos = chosen.Value;
|
||||
_lastPlanned.Add(spawnPos);
|
||||
SpawnCrystal(spawnPos, fsmBehaviour);
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
groundPosition = hit.point;
|
||||
|
||||
if (Physics.CheckSphere(groundPosition + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Vector3 EnforceSeparationFallback(Vector3 desired, Vector3 center)
|
||||
{
|
||||
Vector3 candidate = desired;
|
||||
float step = 0.5f;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
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;
|
||||
|
||||
Vector3 dir = (candidate - center); dir.y = 0f;
|
||||
if (dir.sqrMagnitude < 0.0001f) dir = Vector3.right;
|
||||
candidate = center + dir.normalized * (dir.magnitude + step);
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private void SpawnCrystal(Vector3 position, vIFSMBehaviourController fsmBehaviour)
|
||||
{
|
||||
Quaternion rotation = Quaternion.identity;
|
||||
Transform lookAt = playerTransform != null ? playerTransform : npcTransform;
|
||||
|
||||
if (lookAt != null)
|
||||
{
|
||||
Vector3 lookDirection = (lookAt.position - position).normalized;
|
||||
lookDirection.y = 0;
|
||||
if (lookDirection != Vector3.zero)
|
||||
rotation = Quaternion.LookRotation(lookDirection);
|
||||
}
|
||||
|
||||
var spawned = LeanPool.Spawn(crystalPrefab, position, rotation);
|
||||
|
||||
var shooterAI = spawned.GetComponent<CrystalShooterAI>();
|
||||
if (shooterAI == null)
|
||||
{
|
||||
Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!");
|
||||
}
|
||||
else if (playerTransform != null)
|
||||
{
|
||||
shooterAI.SetTarget(playerTransform);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!showGizmos || npcTransform == null) return;
|
||||
|
||||
Vector3 c = playerTransform ? playerTransform.position : npcTransform.position;
|
||||
|
||||
Gizmos.color = Color.green;
|
||||
DrawWireCircle(c, minSpawnDistance);
|
||||
Gizmos.color = Color.red;
|
||||
DrawWireCircle(c, maxSpawnDistance);
|
||||
|
||||
Gizmos.color = Color.cyan;
|
||||
foreach (var p in _lastPlanned) Gizmos.DrawWireSphere(p + Vector3.up * 0.1f, 0.2f);
|
||||
}
|
||||
|
||||
private void DrawWireCircle(Vector3 center, float radius)
|
||||
{
|
||||
int segments = 32;
|
||||
Vector3 prevPoint = center + new Vector3(radius, 0, 0);
|
||||
for (int i = 1; i <= segments; i++)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayOverlayOnce(Transform owner)
|
||||
{
|
||||
if (overlayClip == null) return;
|
||||
|
||||
if (npcAnimator == null)
|
||||
npcAnimator = owner.GetComponent<Animator>();
|
||||
if (npcAnimator == null) return;
|
||||
|
||||
StopOverlayImmediate(); // safety
|
||||
|
||||
_overlayGraph = PlayableGraph.Create("ActionOverlay(SpawnTurret)");
|
||||
_overlayGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
|
||||
|
||||
_overlayPlayable = AnimationClipPlayable.Create(_overlayGraph, overlayClip);
|
||||
_overlayPlayable.SetApplyFootIK(false);
|
||||
_overlayPlayable.SetApplyPlayableIK(false);
|
||||
_overlayPlayable.SetSpeed(Mathf.Max(0.0001f, overlaySpeed));
|
||||
|
||||
_overlayOutput = AnimationPlayableOutput.Create(_overlayGraph, "AnimOut", npcAnimator);
|
||||
_overlayOutput.SetSourcePlayable(_overlayPlayable);
|
||||
|
||||
_overlayOutput.SetWeight(1f);
|
||||
_overlayGraph.Play();
|
||||
_overlayPlaying = true;
|
||||
|
||||
float len = overlayClip.length / Mathf.Max(0.0001f, overlaySpeed);
|
||||
_overlayStopAtTime = Time.time + len;
|
||||
|
||||
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Overlay clip started via Playables");
|
||||
}
|
||||
|
||||
private void StopOverlayImmediate()
|
||||
{
|
||||
if (_overlayGraph.IsValid())
|
||||
{
|
||||
_overlayGraph.Stop();
|
||||
_overlayGraph.Destroy();
|
||||
}
|
||||
_overlayPlaying = false;
|
||||
}
|
||||
|
||||
private void StopOverlayWithFade()
|
||||
{
|
||||
if (!_overlayPlaying) { StopOverlayImmediate(); return; }
|
||||
if (_overlayOutput.IsOutputNull() == false) _overlayOutput.SetWeight(0f);
|
||||
StopOverlayImmediate();
|
||||
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Overlay clip stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/AI/_Demon/SA_SpawnTurretSmart.cs.meta
Normal file
2
Assets/AI/_Demon/SA_SpawnTurretSmart.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5fcb700ea476bed44becf27143de31bd
|
||||
191
Assets/AI/_Demon/ShieldDamage.cs
Normal file
191
Assets/AI/_Demon/ShieldDamage.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
using Invector;
|
||||
using Invector.vCharacterController;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DemonBoss.Magic
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that adds damage when the player gets too close to the shield.
|
||||
/// Add to the shield object along with Sphere Collider (IsTrigger = true).
|
||||
/// </summary>
|
||||
public class ShieldDamage : MonoBehaviour
|
||||
{
|
||||
[Header("Damage Configuration")]
|
||||
[Tooltip("Damage dealt to the player")]
|
||||
public int damage = 15;
|
||||
|
||||
[Tooltip("Knockback force")]
|
||||
public float knockbackForce = 10f;
|
||||
|
||||
[Tooltip("Player Tag")]
|
||||
public string playerTag = "Player";
|
||||
|
||||
[Tooltip("Time between consecutive hits (so as not to deal damage to every frame)")]
|
||||
public float damageInterval = 1f;
|
||||
|
||||
[Header("Effects")]
|
||||
[Tooltip("Sound effect when dealing damage")]
|
||||
public AudioClip damageSound;
|
||||
|
||||
[Tooltip("Visual effect when dealing damage")]
|
||||
public GameObject damageEffect;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Enable debug logs")]
|
||||
public bool enableDebug = false;
|
||||
|
||||
private AudioSource audioSource;
|
||||
private float lastDamageTime;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (damageSound != null)
|
||||
{
|
||||
audioSource = GetComponent<AudioSource>();
|
||||
if (audioSource == null)
|
||||
{
|
||||
audioSource = gameObject.AddComponent<AudioSource>();
|
||||
audioSource.playOnAwake = false;
|
||||
audioSource.spatialBlend = 1f; // 3D sound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerStay(Collider other)
|
||||
{
|
||||
if (other.CompareTag(playerTag) && Time.time >= lastDamageTime + damageInterval)
|
||||
{
|
||||
DealDamageToPlayer(other);
|
||||
lastDamageTime = Time.time;
|
||||
}
|
||||
}
|
||||
|
||||
private void DealDamageToPlayer(Collider playerCollider)
|
||||
{
|
||||
if (enableDebug) Debug.Log($"[ShieldDamage] I deal {damage} damage to the player: {playerCollider.name}");
|
||||
|
||||
Vector3 hitPoint = playerCollider.ClosestPoint(transform.position);
|
||||
Vector3 hitDirection = (playerCollider.transform.position - transform.position).normalized;
|
||||
|
||||
vDamage damageInfo = new vDamage(damage)
|
||||
{
|
||||
sender = transform,
|
||||
hitPosition = hitPoint
|
||||
};
|
||||
|
||||
if (knockbackForce > 0f)
|
||||
{
|
||||
damageInfo.force = hitDirection * knockbackForce;
|
||||
}
|
||||
|
||||
bool damageDealt = false;
|
||||
|
||||
// 1) vIDamageReceiver
|
||||
var damageReceiver = playerCollider.GetComponent<vIDamageReceiver>() ??
|
||||
playerCollider.GetComponentInParent<vIDamageReceiver>();
|
||||
|
||||
if (damageReceiver != null)
|
||||
{
|
||||
damageReceiver.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
if (enableDebug) Debug.Log("[ShieldDamage] Damage dealt by vIDamageReceiver");
|
||||
}
|
||||
|
||||
// 2) vHealthController
|
||||
if (!damageDealt)
|
||||
{
|
||||
var healthController = playerCollider.GetComponent<vHealthController>() ??
|
||||
playerCollider.GetComponentInParent<vHealthController>();
|
||||
|
||||
if (healthController != null)
|
||||
{
|
||||
healthController.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
if (enableDebug) Debug.Log("[ShieldDamage] Damage dealt by vHealthController");
|
||||
}
|
||||
}
|
||||
|
||||
// 3) vThirdPersonController
|
||||
if (!damageDealt)
|
||||
{
|
||||
var tpc = playerCollider.GetComponent<vThirdPersonController>() ??
|
||||
playerCollider.GetComponentInParent<vThirdPersonController>();
|
||||
|
||||
if (tpc != null)
|
||||
{
|
||||
// SprawdŸ czy to Beyond variant
|
||||
if (tpc is Beyond.bThirdPersonController beyond)
|
||||
{
|
||||
if (!beyond.GodMode && !beyond.isImmortal)
|
||||
{
|
||||
tpc.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
if (enableDebug) Debug.Log("[ShieldDamage] Damage dealt by bThirdPersonController");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebug) Debug.Log("[ShieldDamage] Player is immortal - no damage");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tpc.TakeDamage(damageInfo);
|
||||
damageDealt = true;
|
||||
if (enableDebug) Debug.Log("[ShieldDamage] Damage dealt by vThirdPersonController");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (damageDealt)
|
||||
{
|
||||
PlayEffects(hitPoint);
|
||||
}
|
||||
else if (enableDebug)
|
||||
{
|
||||
Debug.LogWarning("[ShieldDamage] Cannot deal damage - missing required component!");
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayEffects(Vector3 hitPoint)
|
||||
{
|
||||
// Play hit sound
|
||||
if (audioSource != null && damageSound != null)
|
||||
{
|
||||
audioSource.PlayOneShot(damageSound);
|
||||
}
|
||||
|
||||
// Show visual effect
|
||||
if (damageEffect != null)
|
||||
{
|
||||
GameObject effect = Instantiate(damageEffect, hitPoint, Quaternion.identity);
|
||||
Destroy(effect, 2f); // Remove after 2 seconds
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Draw damage range visualization in Scene View
|
||||
/// </summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
// Show damage range
|
||||
Collider col = GetComponent<Collider>();
|
||||
if (col != null && col.isTrigger)
|
||||
{
|
||||
Gizmos.color = Color.red;
|
||||
if (col is SphereCollider sphere)
|
||||
{
|
||||
Gizmos.DrawWireSphere(transform.position, sphere.radius);
|
||||
}
|
||||
else if (col is CapsuleCollider capsule)
|
||||
{
|
||||
// Approximate capsule visualization
|
||||
Gizmos.DrawWireSphere(transform.position, capsule.radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
2
Assets/AI/_Demon/ShieldDamage.cs.meta
Normal file
2
Assets/AI/_Demon/ShieldDamage.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a4b288b220c78b4ca00e13cb8a72d6b
|
||||
38
Assets/AI/_Demon/ShieldScaleUp.cs
Normal file
38
Assets/AI/_Demon/ShieldScaleUp.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class ShieldScaleUp : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Time it takes to fully grow the shield")]
|
||||
public float growDuration = 0.5f;
|
||||
|
||||
[Tooltip("Curve to control growth speed over time")]
|
||||
public AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
|
||||
private float timer = 0f;
|
||||
private Vector3 targetScale;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
targetScale = transform.localScale;
|
||||
transform.localScale = Vector3.zero;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
timer = 0f;
|
||||
transform.localScale = Vector3.zero;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (timer < growDuration)
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(timer / growDuration);
|
||||
|
||||
float scaleValue = scaleCurve.Evaluate(t);
|
||||
|
||||
transform.localScale = targetScale * scaleValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/AI/_Demon/ShieldScaleUp.cs.meta
Normal file
2
Assets/AI/_Demon/ShieldScaleUp.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 64156f4d0e036104895ef313e63c6b09
|
||||
2156
Assets/AI/_Demon/Turret.prefab
Normal file
2156
Assets/AI/_Demon/Turret.prefab
Normal file
File diff suppressed because it is too large
Load Diff
7
Assets/AI/_Demon/Turret.prefab.meta
Normal file
7
Assets/AI/_Demon/Turret.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d20e89d7557c3b41b7ef0e6be87e319
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1453
Assets/AI/_Demon/Waypoints.prefab
Normal file
1453
Assets/AI/_Demon/Waypoints.prefab
Normal file
File diff suppressed because it is too large
Load Diff
7
Assets/AI/_Demon/Waypoints.prefab.meta
Normal file
7
Assets/AI/_Demon/Waypoints.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50edfccd260e3a74daeaf1c0294101fd
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user