diff --git a/Assets/AI/Demon/CrystalShooterAI.cs b/Assets/AI/Demon/CrystalShooterAI.cs
index 8188bf83a..5bb05b369 100644
--- a/Assets/AI/Demon/CrystalShooterAI.cs
+++ b/Assets/AI/Demon/CrystalShooterAI.cs
@@ -13,7 +13,7 @@ namespace DemonBoss.Magic
[Tooltip("Fireball prefab (projectile with its own targeting logic)")]
public GameObject fireballPrefab;
- [Tooltip("Seconds between shots")]
+ [Tooltip("Base seconds between shots")]
public float fireRate = 0.7f;
[Tooltip("Maximum number of shots before auto-despawn")]
@@ -22,6 +22,16 @@ namespace DemonBoss.Magic
[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;
@@ -81,102 +91,69 @@ namespace DemonBoss.Magic
if (muzzle == null)
{
- // Try to find a child named "muzzle"; fallback to self
Transform muzzleChild = crystalTransform.Find("muzzle");
- if (muzzleChild != null)
- {
- muzzle = muzzleChild;
- if (enableDebug) Debug.Log("[CrystalShooterAI] Found muzzle child");
- }
- else
- {
- muzzle = crystalTransform;
- if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Using crystal center as muzzle");
- }
+ muzzle = muzzleChild != null ? muzzleChild : crystalTransform;
}
}
private void Start()
{
- if (enableDebug) Debug.Log("[CrystalShooterAI] Crystal activated");
-
if (autoFindPlayer && target == null)
FindPlayer();
StartShooting();
}
- ///
- /// Update tick: rotate towards target or idle spin.
- ///
+ /// Update tick: rotate towards target or idle spin.
private void Update()
{
if (!isActive) return;
RotateTowardsTarget();
}
- ///
- /// Attempts to find the player by tag (for turret-only aiming).
- ///
+ /// Attempts to find the player by tag (for turret-only aiming).
private void FindPlayer()
{
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
- if (player != null)
- {
- SetTarget(player.transform);
- if (enableDebug) Debug.Log("[CrystalShooterAI] Automatically found player");
- }
- else if (enableDebug)
- {
- Debug.LogWarning("[CrystalShooterAI] Cannot find player with tag: " + playerTag);
- }
+ if (player != null) SetTarget(player.transform);
}
- ///
- /// Sets the turret's aiming target (does NOT propagate to projectiles).
- ///
- public void SetTarget(Transform newTarget)
- {
- target = newTarget;
- if (enableDebug && target != null)
- Debug.Log($"[CrystalShooterAI] Set target: {target.name}");
- }
+ /// Sets the turret's aiming target (does NOT propagate to projectiles).
+ public void SetTarget(Transform newTarget) => target = newTarget;
- ///
- /// Starts the timed shooting routine (fires until maxShots, then despawns).
- ///
+ /// Starts the timed shooting routine (fires until maxShots, then despawns).
public void StartShooting()
{
if (isActive) return;
isActive = true;
shotsFired = 0;
-
shootingCoroutine = StartCoroutine(ShootingCoroutine());
- if (enableDebug) Debug.Log("[CrystalShooterAI] Starting shooting");
}
- ///
- /// Stops the shooting routine immediately.
- ///
+ /// Stops the shooting routine immediately.
public void StopShooting()
{
isActive = false;
-
if (shootingCoroutine != null)
{
StopCoroutine(shootingCoroutine);
shootingCoroutine = null;
}
-
- if (enableDebug) Debug.Log("[CrystalShooterAI] Stopped shooting");
}
///
- /// Main shooting loop: checks aim/range → spawns fireball → waits fireRate.
- /// After finishing, waits a short delay and despawns the turret.
+ /// Main shooting loop with initial spawn stagger and per-shot jitter.
///
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())
@@ -186,18 +163,17 @@ namespace DemonBoss.Magic
lastShotTime = Time.time;
}
- yield return new WaitForSeconds(fireRate);
+ 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);
}
- if (enableDebug) Debug.Log($"[CrystalShooterAI] Finished shooting ({shotsFired} shots)");
-
yield return new WaitForSeconds(despawnDelay);
DespawnCrystal();
}
- ///
- /// Aiming/range gate for firing.
- ///
+ /// Aiming/range gate for firing.
private bool CanShoot()
{
if (target == null) return false;
@@ -211,16 +187,10 @@ namespace DemonBoss.Magic
return angleToTarget <= aimTolerance;
}
- ///
- /// Spawns a fireball oriented towards the turret's current aim direction.
- ///
+ /// Spawns a fireball oriented towards the turret's current aim direction with optional dispersion.
private void FireFireball()
{
- if (fireballPrefab == null || muzzle == null)
- {
- if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Missing fireball prefab or muzzle");
- return;
- }
+ if (fireballPrefab == null || muzzle == null) return;
Vector3 shootDirection;
if (target != null)
@@ -228,28 +198,24 @@ namespace DemonBoss.Magic
Vector3 targetCenter = target.position + Vector3.up * 1f;
shootDirection = (targetCenter - muzzle.position).normalized;
}
- else
+ else shootDirection = crystalTransform.forward;
+
+ // Apply small aim jitter (random yaw/pitch) to avoid perfect sync volleys
+ if (aimJitterDegrees > 0f)
{
- shootDirection = crystalTransform.forward;
+ 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();
-
- if (enableDebug)
- {
- Debug.Log($"[CrystalShooterAI] Shot #{shotsFired + 1} at {spawnPosition} dir: {shootDirection}");
- Debug.DrawRay(spawnPosition, shootDirection * 8f, Color.red, 2f);
- }
}
- ///
- /// Plays muzzle VFX and shoot SFX (if enabled).
- ///
+ /// Plays muzzle VFX and shoot SFX (if enabled).
private void PlayShootEffects()
{
if (!useShootEffects) return;
@@ -261,14 +227,10 @@ namespace DemonBoss.Magic
}
if (audioSource != null && shootSound != null)
- {
audioSource.PlayOneShot(shootSound);
- }
}
- ///
- /// Smooth yaw rotation towards target; idles by spinning when no target.
- ///
+ /// Smooth yaw rotation towards target; idles by spinning when no target.
private void RotateTowardsTarget()
{
if (target != null)
@@ -292,24 +254,15 @@ namespace DemonBoss.Magic
}
}
- ///
- /// Despawns the turret via Lean Pool.
- ///
+ /// Despawns the turret via Lean Pool.
public void DespawnCrystal()
{
- if (enableDebug) Debug.Log("[CrystalShooterAI] Despawning crystal");
StopShooting();
LeanPool.Despawn(gameObject);
}
- ///
- /// Forces immediate despawn (e.g., boss death).
- ///
- public void ForceDespawn()
- {
- if (enableDebug) Debug.Log("[CrystalShooterAI] Forced despawn");
- DespawnCrystal();
- }
+ /// Forces immediate despawn (e.g., boss death).
+ public void ForceDespawn() => DespawnCrystal();
/// Returns crystal state information.
public bool IsActive() => isActive;
@@ -320,9 +273,6 @@ namespace DemonBoss.Magic
public float GetTimeSinceLastShot() => Time.time - lastShotTime;
- ///
- /// Gizmos for range and aim visualization.
- ///
private void OnDrawGizmosSelected()
{
if (!showGizmos) return;
@@ -330,24 +280,6 @@ namespace DemonBoss.Magic
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, maxShootingRange);
- if (target != null)
- {
- Gizmos.color = Color.yellow;
- Gizmos.DrawLine(transform.position, target.position);
-
- if (muzzle != null)
- {
- Gizmos.color = Color.green;
- Vector3 forward = transform.forward * 5f;
- Vector3 right = Quaternion.AngleAxis(aimTolerance, transform.up) * forward;
- Vector3 left = Quaternion.AngleAxis(-aimTolerance, transform.up) * forward;
-
- Gizmos.DrawLine(muzzle.position, muzzle.position + right);
- Gizmos.DrawLine(muzzle.position, muzzle.position + left);
- Gizmos.DrawLine(muzzle.position, muzzle.position + forward);
- }
- }
-
if (muzzle != null)
{
Gizmos.color = Color.blue;
diff --git a/Assets/AI/Demon/Crystals_red 1.mat b/Assets/AI/Demon/Crystals_red 1.mat
new file mode 100644
index 000000000..a31071947
--- /dev/null
+++ b/Assets/AI/Demon/Crystals_red 1.mat
@@ -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
diff --git a/Assets/AI/Demon/Crystals_red 1.mat.meta b/Assets/AI/Demon/Crystals_red 1.mat.meta
new file mode 100644
index 000000000..b1060c5f1
--- /dev/null
+++ b/Assets/AI/Demon/Crystals_red 1.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c11c6b446cf6c4b4cb5b5cd72263e342
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/AI/Demon/Crystals_red 2.mat b/Assets/AI/Demon/Crystals_red 2.mat
new file mode 100644
index 000000000..afd862173
--- /dev/null
+++ b/Assets/AI/Demon/Crystals_red 2.mat
@@ -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
diff --git a/Assets/AI/Demon/Crystals_red 2.mat.meta b/Assets/AI/Demon/Crystals_red 2.mat.meta
new file mode 100644
index 000000000..39c7d1e96
--- /dev/null
+++ b/Assets/AI/Demon/Crystals_red 2.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f94eea72c30ac2e45ab55894421ea48c
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/AI/Demon/DemonShield.prefab b/Assets/AI/Demon/DemonShield.prefab
index 9c84f5a5b..1f2b947c3 100644
--- a/Assets/AI/Demon/DemonShield.prefab
+++ b/Assets/AI/Demon/DemonShield.prefab
@@ -12,7 +12,7 @@ GameObject:
- component: {fileID: 8933020562711606400}
- component: {fileID: 2735702040698985183}
- component: {fileID: 4889297222623638116}
- m_Layer: 0
+ m_Layer: 30
m_Name: Shield
m_TagString: Untagged
m_Icon: {fileID: 0}
@@ -4828,7 +4828,7 @@ GameObject:
- component: {fileID: 1705028462786019458}
- component: {fileID: 636964402995770057}
- component: {fileID: 3712794430867809983}
- m_Layer: 0
+ m_Layer: 30
m_Name: TriggerEnter
m_TagString: Untagged
m_Icon: {fileID: 0}
@@ -4900,7 +4900,7 @@ GameObject:
m_Component:
- component: {fileID: 5385552833772178802}
- component: {fileID: 6288768711713787120}
- m_Layer: 0
+ m_Layer: 30
m_Name: PerPlatformSettings
m_TagString: Untagged
m_Icon: {fileID: 0}
@@ -4948,7 +4948,10 @@ GameObject:
m_Component:
- component: {fileID: 7058331355936229169}
- component: {fileID: 1024337125332675931}
- m_Layer: 0
+ - component: {fileID: -842443613141667859}
+ - component: {fileID: 4897060740913123605}
+ - component: {fileID: 6803317444044382316}
+ m_Layer: 30
m_Name: DemonShield
m_TagString: Untagged
m_Icon: {fileID: 0}
@@ -4998,7 +5001,87 @@ CapsuleCollider:
m_Radius: 2.5
m_Height: 2.5
m_Direction: 1
- m_Center: {x: 0, y: 1.5, z: 0}
+ m_Center: {x: 0, y: 1.5, z: -0.8}
+--- !u!114 &-842443613141667859
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4089514887418515818}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 64156f4d0e036104895ef313e63c6b09, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ growDuration: 0.5
+ scaleCurve:
+ serializedVersion: 2
+ m_Curve:
+ - serializedVersion: 3
+ time: 0
+ value: 0
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0
+ outWeight: 0
+ - serializedVersion: 3
+ time: 1
+ value: 1
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0
+ outWeight: 0
+ m_PreInfinity: 2
+ m_PostInfinity: 2
+ m_RotationOrder: 4
+--- !u!136 &4897060740913123605
+CapsuleCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4089514887418515818}
+ 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: 3
+ m_Height: 3
+ m_Direction: 1
+ m_Center: {x: 0, y: 1.5, z: -0.8}
+--- !u!114 &6803317444044382316
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4089514887418515818}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 3a4b288b220c78b4ca00e13cb8a72d6b, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ damage: 15
+ knockbackForce: 10
+ playerTag: Player
+ damageInterval: 1
+ damageSound: {fileID: 8300000, guid: 0af332725d9792840a4caa29e8991d08, type: 3}
+ damageEffect: {fileID: 1606542427775616, guid: 0de6da49dab6f8b4d93ab32a4cb441af,
+ type: 3}
+ enableDebug: 0
--- !u!1 &5185508652979790054
GameObject:
m_ObjectHideFlags: 0
@@ -5011,7 +5094,7 @@ GameObject:
- component: {fileID: 2972078956396372929}
- component: {fileID: 2302274119979875947}
- component: {fileID: 7352519013254002557}
- m_Layer: 0
+ m_Layer: 30
m_Name: ShieldAdd
m_TagString: Untagged
m_Icon: {fileID: 0}
@@ -9833,7 +9916,7 @@ GameObject:
- component: {fileID: 4294035958862554727}
- component: {fileID: 8165694420557042206}
- component: {fileID: 5904661144955708609}
- m_Layer: 0
+ m_Layer: 30
m_Name: ShieldFringe
m_TagString: Untagged
m_Icon: {fileID: 0}
@@ -14651,10 +14734,34 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 7058331355936229169}
m_Modifications:
+ - target: {fileID: 1137363236680030, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 1300024870466346, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 1530244028065882, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 1606657272102914, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 1849043180036032, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
- target: {fileID: 1881634734537756, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
propertyPath: m_Name
value: shield3
objectReference: {fileID: 0}
+ - target: {fileID: 1881634734537756, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
- target: {fileID: 4855141465030352, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
propertyPath: m_LocalPosition.x
value: -0.76840776
@@ -14748,14 +14855,38 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 7058331355936229169}
m_Modifications:
+ - target: {fileID: 1137363236680030, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 1300024870466346, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 1530244028065882, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
- target: {fileID: 1606657272102914, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
propertyPath: m_Name
value: Shield02
objectReference: {fileID: 0}
+ - target: {fileID: 1606657272102914, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 1849043180036032, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
- target: {fileID: 1881634734537756, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
propertyPath: m_Name
value: sield2
objectReference: {fileID: 0}
+ - target: {fileID: 1881634734537756, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
- target: {fileID: 4855141465030352, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
propertyPath: m_LocalPosition.x
value: 0.561755
@@ -14849,10 +14980,34 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 7058331355936229169}
m_Modifications:
+ - target: {fileID: 1137363236680030, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 1300024870466346, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 1530244028065882, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 1606657272102914, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 1849043180036032, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
- target: {fileID: 1881634734537756, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
propertyPath: m_Name
value: shield3
objectReference: {fileID: 0}
+ - target: {fileID: 1881634734537756, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
- target: {fileID: 4855141465030352, guid: 5b389d585d7681948a86765d14232bdb, type: 3}
propertyPath: m_LocalPosition.x
value: -0.123969555
diff --git a/Assets/AI/Demon/DestroyableTurret.cs b/Assets/AI/Demon/DestroyableTurret.cs
new file mode 100644
index 000000000..949ff2e9f
--- /dev/null
+++ b/Assets/AI/Demon/DestroyableTurret.cs
@@ -0,0 +1,367 @@
+using Invector;
+using Lean.Pool;
+using UnityEngine;
+
+namespace DemonBoss.Magic
+{
+ ///
+ /// 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)
+ ///
+ 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(); }
+
+ ///
+ /// Initialize components and validate setup
+ ///
+ private void Awake()
+ {
+ InitializeComponents();
+ InitializeHealth();
+ }
+
+ ///
+ /// Find and configure required components
+ ///
+ private void InitializeComponents()
+ {
+ // Find components
+ shooterAI = GetComponent();
+ turretRenderer = GetComponent();
+
+ // Store original material
+ if (turretRenderer != null)
+ {
+ originalMaterial = turretRenderer.material;
+ }
+
+ // Setup AudioSource
+ audioSource = GetComponent();
+ if (audioSource == null && (destructionSound != null || hitSound != null))
+ {
+ audioSource = gameObject.AddComponent();
+ audioSource.playOnAwake = false;
+ audioSource.spatialBlend = 1f; // 3D sound
+ }
+
+ // Validate collider setup
+ Collider col = GetComponent();
+ if (col != null && col.isTrigger && enableDebug)
+ {
+ Debug.LogWarning("[DestroyableTurret] Collider should not be trigger for damage system!");
+ }
+ }
+
+ ///
+ /// Setup initial health values
+ ///
+ private void InitializeHealth()
+ {
+ currentHealth = maxHealth;
+ maxHealthRecovery = maxHealth;
+ currentHealthRecovery = 0;
+
+ if (enableDebug) Debug.Log($"[DestroyableTurret] Initialized with {currentHealth}/{maxHealth} HP");
+ }
+
+ #region vIDamageReceiver Implementation
+
+ ///
+ /// Handle incoming damage from Invector damage system
+ ///
+ 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}");
+ }
+
+ ///
+ /// Handle incoming damage with hit reaction parameter
+ ///
+ public void TakeDamage(vDamage damage, bool hitReaction)
+ {
+ TakeDamage(damage);
+ }
+
+ #endregion vIDamageReceiver Implementation
+
+ #region vIHealthController Implementation
+
+ ///
+ /// Apply damage to the turret and handle destruction
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Change health by specified amount (positive for healing, negative for damage)
+ ///
+ 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}");
+ }
+ }
+
+ ///
+ /// Modify maximum health value
+ ///
+ public void ChangeMaxHealth(int value)
+ {
+ maxHealth = Mathf.Max(1, maxHealth + value);
+ currentHealth = Mathf.Min(currentHealth, maxHealth);
+ }
+
+ #endregion vIHealthController Implementation
+
+ ///
+ /// Play sound and visual effects when receiving damage
+ ///
+ private void PlayHitEffects()
+ {
+ // Play hit sound
+ if (audioSource != null && hitSound != null)
+ {
+ audioSource.PlayOneShot(hitSound);
+ }
+ }
+
+ ///
+ /// Update visual appearance based on current health
+ ///
+ 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");
+ }
+ }
+ }
+
+ ///
+ /// Handle turret destruction sequence
+ ///
+ 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));
+ }
+
+ ///
+ /// Play visual and audio effects for turret destruction
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Coroutine to destroy turret after specified delay
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Force immediate turret destruction (e.g., when boss dies)
+ ///
+ public void ForceDestroy()
+ {
+ if (enableDebug) Debug.Log("[DestroyableTurret] Forced destruction");
+ currentHealth = 0;
+ //isDead = true;
+ DestroyTurret();
+ }
+
+ ///
+ /// Returns current health as percentage of maximum health
+ ///
+ public float GetHealthPercentage()
+ {
+ return maxHealth > 0 ? (float)currentHealth / maxHealth : 0f;
+ }
+
+#if UNITY_EDITOR
+
+ ///
+ /// Draw turret status visualization in Scene View
+ ///
+ 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 UNITY_EDITOR
+ 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();
+ }
+
+#endif
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/Demon/DestroyableTurret.cs.meta b/Assets/AI/Demon/DestroyableTurret.cs.meta
new file mode 100644
index 000000000..d00bb858e
--- /dev/null
+++ b/Assets/AI/Demon/DestroyableTurret.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 06579ea47ceeddd42a05f7720c15b5de
\ No newline at end of file
diff --git a/Assets/AI/Demon/FireBall.prefab b/Assets/AI/Demon/FireBall.prefab
index 8a9f0d738..aea7a3f0f 100644
--- a/Assets/AI/Demon/FireBall.prefab
+++ b/Assets/AI/Demon/FireBall.prefab
@@ -11,7 +11,7 @@ GameObject:
- component: {fileID: 4577187839491108}
- component: {fileID: 198086061384069858}
- component: {fileID: 199127982807948490}
- m_Layer: 2
+ m_Layer: 30
m_Name: FireEmbers (4)
m_TagString: Untagged
m_Icon: {fileID: 0}
@@ -4768,7 +4768,7 @@ GameObject:
- component: {fileID: 199577645087675124}
- component: {fileID: -5586632368230897359}
- component: {fileID: 6785567375430979834}
- m_Layer: 0
+ m_Layer: 30
m_Name: FireBall
m_TagString: Untagged
m_Icon: {fileID: 0}
@@ -6246,8 +6246,8 @@ ParticleSystem:
rowMode: 1
sprites:
- sprite: {fileID: 0}
- flipU: 0.5
- flipV: 0.5
+ flipU: 0
+ flipV: 0
VelocityModule:
enabled: 0
x:
@@ -9584,7 +9584,7 @@ ParticleSystemRenderer:
m_ShadowBias: 0
m_RenderAlignment: 0
m_Pivot: {x: 0, y: 0, z: 0}
- m_Flip: {x: 0, y: 0, z: 0}
+ m_Flip: {x: 0.5, y: 0.5, z: 0}
m_EnableGPUInstancing: 1
m_ApplyActiveColorSpace: 1
m_AllowRoll: 1
@@ -9622,10 +9622,10 @@ CapsuleCollider:
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 2
- m_Radius: 0.5
- m_Height: 0.1
+ m_Radius: 1
+ m_Height: 0
m_Direction: 1
- m_Center: {x: 0.31801516, y: 0, z: 0}
+ m_Center: {x: 0, y: 0, z: 0}
--- !u!114 &6785567375430979834
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -9639,10 +9639,11 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
targetTag: Player
+ targetHeightOffset: 1
speed: 6
- lockTime: 15
- maxLifeTime: 30
+ lockTime: 0.5
+ maxLifeTime: 60
arrivalTolerance: 0.25
- damage: 25
+ damage: 5
knockbackForce: 5
enableDebug: 0
diff --git a/Assets/AI/Demon/Meteor.prefab b/Assets/AI/Demon/Meteor.prefab
index e3b817858..241406118 100644
--- a/Assets/AI/Demon/Meteor.prefab
+++ b/Assets/AI/Demon/Meteor.prefab
@@ -11,7 +11,7 @@ GameObject:
- component: {fileID: 4577187839491108}
- component: {fileID: 198086061384069858}
- component: {fileID: 199127982807948490}
- m_Layer: 2
+ m_Layer: 30
m_Name: FireEmbers (4)
m_TagString: Untagged
m_Icon: {fileID: 0}
@@ -4769,7 +4769,7 @@ GameObject:
- component: {fileID: 199577645087675124}
- component: {fileID: -5586632368230897359}
- component: {fileID: -745564043032181229}
- m_Layer: 0
+ m_Layer: 30
m_Name: Meteor
m_TagString: Untagged
m_Icon: {fileID: 0}
@@ -9639,30 +9639,35 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0d6ec41ae03923f45a963ca66dbb6c56, type: 3}
m_Name:
m_EditorClassIdentifier:
- targetTag: Player
+ useOverrideImpactPoint: 0
+ overrideImpactPoint: {x: 0, y: 0, z: 0}
snapImpactToGround: 1
groundMask:
serializedVersion: 2
m_Bits: 1410334711
- spawnHeightAboveTarget: 12
- entryAngleDownFromHorizontal: 20
- azimuthAimWeight: 0.85
+ groundSnapRayStart: 3
+ groundSnapRayLength: 30
+ impactPoint: {x: 0, y: 0, z: 0}
speed: 30
- gravityLikeAcceleration: 1
- rotateToVelocity: 1
- maxLifeTime: 12
+ gravityLikePull: 14
+ spawnHeightAboveTarget: 12
+ arriveEpsilon: 0.35
+ maxLifetime: 30
+ traceRadius: 0.45
+ stopOnLayers:
+ serializedVersion: 2
+ m_Bits: 1410334711
+ damageLayers:
+ serializedVersion: 2
+ m_Bits: 1410334711
explosionRadius: 5
- explosionMask:
- serializedVersion: 2
- m_Bits: 1410334711
- aoeOnlyTargetTag: 0
- castRadius: 0.25
- collisionMask:
- serializedVersion: 2
- m_Bits: 1410334711
damage: 40
knockbackForce: 5
- enableDebug: 0
+ impactVfxPrefab: {fileID: 1606542427775616, guid: 0de6da49dab6f8b4d93ab32a4cb441af,
+ type: 3}
+ onSpawn:
+ m_PersistentCalls:
+ m_Calls: []
onImpact:
m_PersistentCalls:
m_Calls: []
@@ -9678,6 +9683,26 @@ PrefabInstance:
propertyPath: m_Name
value: Meteor
objectReference: {fileID: 0}
+ - target: {fileID: 100000, guid: 638572418cf33664f88dae09c5bf7652, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 100048, guid: 638572418cf33664f88dae09c5bf7652, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 100050, guid: 638572418cf33664f88dae09c5bf7652, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 100052, guid: 638572418cf33664f88dae09c5bf7652, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
+ - target: {fileID: 100054, guid: 638572418cf33664f88dae09c5bf7652, type: 3}
+ propertyPath: m_Layer
+ value: 30
+ objectReference: {fileID: 0}
- target: {fileID: 400000, guid: 638572418cf33664f88dae09c5bf7652, type: 3}
propertyPath: m_LocalScale.x
value: 20
diff --git a/Assets/AI/Demon/MeteorProjectile.cs b/Assets/AI/Demon/MeteorProjectile.cs
index 734ca5765..3046ea746 100644
--- a/Assets/AI/Demon/MeteorProjectile.cs
+++ b/Assets/AI/Demon/MeteorProjectile.cs
@@ -6,278 +6,287 @@ using UnityEngine.Events;
namespace DemonBoss.Magic
{
+ ///
+ /// Enhanced meteor projectile with fireball-like tracking mechanics
+ /// Can either track player or fly to a locked impact point
+ ///
public class MeteorProjectile : MonoBehaviour
{
- #region Configuration
+ #region Inspector: Targeting
[Header("Targeting")]
- [Tooltip("Tag of the target to pick impact point from on spawn")]
+ [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("If true, raycasts down from impact point to find ground (better visuals)")]
+ [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("LayerMask used when snapping impact point to ground")]
+ [Tooltip("Layers considered 'ground'")]
public LayerMask groundMask = ~0;
- [Header("Entry Geometry")]
- [Tooltip("If > 0, teleports start Y above impact point by this height (keeps current XZ). 0 = keep original height.")]
- public float spawnHeightAboveTarget = 12f;
+ [ReadOnlyInInspector] public Vector3 currentTargetPoint;
- [Tooltip("Entry angle measured DOWN from horizontal (e.g. 15° = shallow, 45° = steeper)")]
- [Range(0f, 89f)]
- public float entryAngleDownFromHorizontal = 20f;
+ #endregion Inspector: Targeting
- [Tooltip("How closely to aim towards the target in XZ before tilting down (0..1). 1 = purely towards target, 0 = purely downward.")]
- [Range(0f, 1f)]
- public float azimuthAimWeight = 0.85f;
+ #region Inspector: Flight
- [Header("Movement")]
- [Tooltip("Initial speed magnitude in units per second")]
- public float speed = 30f;
+ [Header("Flight")]
+ [Tooltip("Movement speed in m/s")]
+ public float speed = 25f;
- [Tooltip("Extra downward acceleration to add a slight curve (0 = disabled)")]
- public float gravityLikeAcceleration = 0f;
+ [Tooltip("Time to track player before locking to position")]
+ public float trackingTime = 1.5f;
- [Tooltip("Rotate the meteor mesh to face velocity")]
- public bool rotateToVelocity = true;
+ [Tooltip("Distance threshold to consider arrived")]
+ public float arriveEpsilon = 0.5f;
- [Tooltip("Seconds before the meteor auto-despawns")]
- public float maxLifeTime = 12f;
+ [Tooltip("Max lifetime in seconds")]
+ public float maxLifetime = 15f;
- [Header("Impact / AOE")]
- [Tooltip("Explosion radius at impact (0 = no AOE, only single target)")]
- public float explosionRadius = 0f;
+ #endregion Inspector: Flight
- [Tooltip("Layers that should receive AOE damage")]
- public LayerMask explosionMask = ~0;
+ #region Inspector: Collision & Damage
- [Tooltip("If true, AOE will only damage colliders with the same tag as targetTag")]
- public bool aoeOnlyTargetTag = false;
+ [Header("Collision & Damage")]
+ [Tooltip("Collision detection radius")]
+ public float collisionRadius = 0.8f;
- [Tooltip("Sphere radius for continuous collision checks")]
- public float castRadius = 0.25f;
+ [Tooltip("Layers that stop the meteor")]
+ public LayerMask stopOnLayers = ~0;
- [Tooltip("Layers that block the meteor during flight (ground/walls/etc.)")]
- public LayerMask collisionMask = ~0;
+ [Tooltip("Layers that take damage")]
+ public LayerMask damageLayers = ~0;
- [Tooltip("Damage that will be dealt to Player")]
- public float damage = 40.0f;
+ [Tooltip("Explosion damage radius")]
+ public float explosionRadius = 4f;
- [Tooltip("Knockback that will be applied to Player")]
- public float knockbackForce = 5.0f;
+ [Tooltip("Base damage")]
+ public int damage = 35;
- [Header("Debug")]
- [Tooltip("Enable verbose logs and debug rays")]
- public bool enableDebug = false;
+ [Tooltip("Knockback force")]
+ public float knockbackForce = 12f;
- [Header("Events")]
- [Tooltip("Invoked once on any impact (use for VFX/SFX/CameraShake)")]
+ #endregion Inspector: Collision & Damage
+
+ #region Inspector: Effects
+
+ [Header("Effects & Events")]
+ public GameObject impactVfxPrefab;
+
+ public UnityEvent onSpawn;
public UnityEvent onImpact;
- #endregion Configuration
+ [Header("Debug")]
+ public bool enableDebug = false;
+
+ #endregion Inspector: Effects
#region Runtime
- private Vector3 impactPoint;
- private Vector3 velocityDir;
- private Vector3 velocity;
- private float timer = 0f;
- private bool hasDealtDamage = false;
- private bool hasImpacted = false;
- private Vector3 prevPos;
+ 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
- ///
- /// Resets runtime state, locks the impact point, computes entry direction and initial velocity.
- ///
private void OnEnable()
{
- timer = 0f;
- hasDealtDamage = false;
- hasImpacted = false;
+ ResetState();
+ InitializeTargeting();
+ onSpawn?.Invoke();
- // Acquire player and lock impact point
- Vector3 targetPos = GetPlayerPosition();
- impactPoint = snapImpactToGround ? SnapPointToGround(targetPos) : targetPos;
-
- // Optionally start from above to guarantee top-down feel
- if (spawnHeightAboveTarget > 0f)
- {
- Vector3 p = transform.position;
- p.y = impactPoint.y + spawnHeightAboveTarget;
- transform.position = p;
- }
-
- Vector3 toImpactXZ = new Vector3(impactPoint.x, transform.position.y, impactPoint.z) - transform.position;
- Vector3 azimuthDir = toImpactXZ.sqrMagnitude > 0.0001f ? toImpactXZ.normalized : transform.forward;
-
- Vector3 tiltAxis = Vector3.Cross(Vector3.up, azimuthDir);
- if (tiltAxis.sqrMagnitude < 0.0001f) tiltAxis = Vector3.right;
- Quaternion downTilt = Quaternion.AngleAxis(-entryAngleDownFromHorizontal, tiltAxis);
- Vector3 tiltedDir = (downTilt * azimuthDir).normalized;
-
- velocityDir = Vector3.Slerp(Vector3.down, tiltedDir, Mathf.Clamp01(azimuthAimWeight)).normalized;
-
- velocity = velocityDir * Mathf.Max(0f, speed);
-
- if (rotateToVelocity && velocity.sqrMagnitude > 0.0001f)
- transform.rotation = Quaternion.LookRotation(velocity.normalized);
-
- if (enableDebug)
- {
- Debug.Log($"[Meteor] Impact={impactPoint}, start={transform.position}, dir={velocityDir}");
- Debug.DrawLine(transform.position, transform.position + velocityDir * 6f, Color.red, 3f);
- Debug.DrawLine(transform.position, impactPoint, Color.yellow, 2f);
- }
-
- prevPos = transform.position;
+ if (enableDebug) Debug.Log($"[MeteorProjectile] Spawned at {transform.position}");
}
- ///
- /// Integrates motion with optional downward acceleration, performs a SphereCast for continuous collision,
- /// rotates to face velocity, and handles lifetime expiry.
- ///
private void Update()
{
- timer += Time.deltaTime;
+ if (_hasImpacted) return;
- // Downward "gravity-like" acceleration
- if (gravityLikeAcceleration > 0f)
- velocity += Vector3.down * gravityLikeAcceleration * Time.deltaTime;
+ _lifetime += Time.deltaTime;
- Vector3 nextPos = transform.position + velocity * Time.deltaTime;
-
- // Continuous collision: SphereCast from current position toward next
- Vector3 castDir = nextPos - transform.position;
- float castDist = castDir.magnitude;
- if (castDist > 0.0001f)
+ // Check lifetime limit
+ if (_lifetime >= maxLifetime)
{
- if (Physics.SphereCast(transform.position, castRadius, castDir.normalized,
- out RaycastHit hit, castDist, collisionMask, QueryTriggerInteraction.Ignore))
- {
- transform.position = hit.point;
- Impact(hit.collider, hit.point, hit.normal);
- return;
- }
+ if (enableDebug) Debug.Log("[MeteorProjectile] Lifetime expired");
+ DoImpact(transform.position);
+ return;
}
- // No hit — apply movement
- transform.position = nextPos;
-
- // Face velocity if requested
- if (rotateToVelocity && velocity.sqrMagnitude > 0.0001f)
- transform.rotation = Quaternion.LookRotation(velocity.normalized);
-
- prevPos = transform.position;
-
- // Auto-despawn on lifetime cap
- if (timer >= maxLifeTime)
+ // Handle tracking to lock transition
+ if (!_isLocked && _lifetime >= trackingTime)
{
- if (enableDebug) Debug.Log("[Meteor] Max lifetime reached → Despawn");
- Despawn();
+ LockTarget();
}
- }
- ///
- /// Trigger-based contact: funnels into unified Impact().
- ///
- private void OnTriggerEnter(Collider other)
- {
- // Even if not the intended target, a meteor typically impacts anything it touches
- Impact(other, transform.position, -SafeNormal(velocity));
- }
-
- ///
- /// Collision-based contact: funnels into unified Impact().
- ///
- private void OnCollisionEnter(Collision collision)
- {
- var cp = collision.contacts.Length > 0 ? collision.contacts[0] : default;
- Impact(collision.collider, cp.point != default ? cp.point : transform.position, cp.normal != default ? cp.normal : Vector3.up);
+ // Update target position and move
+ UpdateTargetPosition();
+ MoveTowardsTarget();
+ CheckCollisions();
}
#endregion Unity
- #region Helpers: Target / Ground
+ #region Targeting & Movement
- ///
- /// Gets the target's current position (by tag). If not found, projects forward from current position.
- ///
- private Vector3 GetPlayerPosition()
+ private void ResetState()
{
- GameObject playerObj = GameObject.FindGameObjectWithTag(targetTag);
- if (playerObj != null)
- return playerObj.transform.position;
-
- if (enableDebug) Debug.LogWarning($"[Meteor] No target with tag '{targetTag}' found, using forward guess.");
- return transform.position + transform.forward * 10f;
+ _hasImpacted = false;
+ _isLocked = false;
+ _lifetime = 0f;
+ _lockedTarget = Vector3.zero;
}
- ///
- /// Raycasts down from above the source point to find ground; returns original point if no hit.
- ///
- private Vector3 SnapPointToGround(Vector3 source)
+ private void InitializeTargeting()
{
- Vector3 start = source + Vector3.up * 50f;
- if (Physics.Raycast(start, Vector3.down, out RaycastHit hit, 200f, groundMask, QueryTriggerInteraction.Ignore))
- return hit.point;
+ if (useOverrideImpactPoint)
+ {
+ _lockedTarget = snapImpactToGround ? SnapToGround(overrideImpactPoint) : overrideImpactPoint;
+ _isLocked = true;
+ currentTargetPoint = _lockedTarget;
- return source;
+ 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}");
+ }
+ }
}
- ///
- /// Returns a safe normalized version of a vector (falls back to Vector3.down if tiny).
- ///
- private static Vector3 SafeNormal(Vector3 v)
+ private void LockTarget()
{
- return v.sqrMagnitude > 0.0001f ? v.normalized : Vector3.down;
+ 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}");
}
- #endregion Helpers: Target / Ground
+ 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
- ///
- /// Unified impact handler (idempotent). Deals single-target damage if applicable,
- /// optional AOE damage, invokes events, and despawns.
- ///
- private void Impact(Collider hitCol, Vector3 hitPoint, Vector3 hitNormal)
+ private void DoImpact(Vector3 impactPos)
{
- if (hasImpacted) return;
- hasImpacted = true;
+ if (_hasImpacted) return;
+ _hasImpacted = true;
- if (enableDebug)
- Debug.Log($"[Meteor] Impact with {(hitCol ? hitCol.name : "null")} at {hitPoint}");
+ if (enableDebug) Debug.Log($"[MeteorProjectile] Impact at {impactPos}");
- // Single target (original logic)
- if (!hasDealtDamage && hitCol != null && hitCol.CompareTag(targetTag))
- DealDamageToTarget(hitCol);
+ // Spawn VFX
+ if (impactVfxPrefab != null)
+ {
+ var vfx = LeanPool.Spawn(impactVfxPrefab, impactPos, Quaternion.identity);
+ // Auto-despawn VFX after 5 seconds
+ LeanPool.Despawn(vfx, 5f);
+ }
- // AOE (if enabled)
- if (explosionRadius > 0f)
- DealAreaDamage(hitPoint);
-
- // Hook for VFX/SFX/CameraShake
onImpact?.Invoke();
- Despawn();
+ // 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);
}
- ///
- /// Deals damage to the intended single target using Invector interfaces/components.
- ///
- private void DealDamageToTarget(Collider targetCollider)
+ private void DealDamageToTarget(Collider targetCollider, Vector3 hitPoint)
{
- if (enableDebug) Debug.Log($"[MeteorDamage] Dealing {damage} damage to: {targetCollider.name}");
+ Vector3 hitDirection = (targetCollider.bounds.center - hitPoint).normalized;
+ if (hitDirection.sqrMagnitude < 0.0001f) hitDirection = Vector3.up;
- Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
- Vector3 hitDirection = GetHitDirection(targetCollider);
-
- vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage))
+ vDamage damageInfo = new vDamage(damage)
{
sender = transform,
hitPosition = hitPoint
@@ -288,185 +297,104 @@ namespace DemonBoss.Magic
bool damageDealt = false;
- // vIDamageReceiver
+ // Try vIDamageReceiver first
var receiver = targetCollider.GetComponent() ??
- targetCollider.GetComponentInParent();
- if (receiver != null)
+ targetCollider.GetComponentInParent();
+ if (receiver != null && !damageDealt)
{
receiver.TakeDamage(damageInfo);
damageDealt = true;
- hasDealtDamage = true;
- if (enableDebug) Debug.Log("[MeteorDamage] Damage via vIDamageReceiver");
}
- // vHealthController
+ // Fallback to vHealthController
if (!damageDealt)
{
- var health = targetCollider.GetComponent() ??
- targetCollider.GetComponentInParent();
- if (health != null)
+ var hc = targetCollider.GetComponent() ??
+ targetCollider.GetComponentInParent();
+ if (hc != null)
{
- health.TakeDamage(damageInfo);
+ hc.TakeDamage(damageInfo);
damageDealt = true;
- hasDealtDamage = true;
- if (enableDebug) Debug.Log("[MeteorDamage] Damage via vHealthController");
}
}
- // vThirdPersonController (including Beyond variant)
+ // Fallback to vThirdPersonController
if (!damageDealt)
{
var tpc = targetCollider.GetComponent() ??
- targetCollider.GetComponentInParent();
+ targetCollider.GetComponentInParent();
if (tpc != null)
{
+ // Handle Beyond variant
if (tpc is Beyond.bThirdPersonController beyond)
{
if (!beyond.GodMode && !beyond.isImmortal)
{
tpc.TakeDamage(damageInfo);
damageDealt = true;
- hasDealtDamage = true;
- if (enableDebug) Debug.Log("[MeteorDamage] Damage via bThirdPersonController");
- }
- else if (enableDebug)
- {
- Debug.Log("[MeteorDamage] Target is immortal/GodMode – no damage");
}
}
else
{
tpc.TakeDamage(damageInfo);
damageDealt = true;
- hasDealtDamage = true;
- if (enableDebug) Debug.Log("[MeteorDamage] Damage via vThirdPersonController");
}
}
}
- if (!damageDealt && enableDebug)
- Debug.LogWarning("[MeteorDamage] No valid damage receiver found!");
- }
-
- ///
- /// Deals AOE damage to all colliders within explosionRadius using Invector-compatible logic.
- ///
- private void DealAreaDamage(Vector3 center)
- {
- Collider[] hits = Physics.OverlapSphere(center, explosionRadius, explosionMask, QueryTriggerInteraction.Ignore);
- foreach (var col in hits)
+ // Apply physics force
+ var rb = targetCollider.attachedRigidbody;
+ if (rb != null && knockbackForce > 0f)
{
- if (col == null) continue;
- if (aoeOnlyTargetTag && !col.CompareTag(targetTag)) continue;
-
- // Avoid double-hitting the same single target if already processed
- if (col.CompareTag(targetTag) && hasDealtDamage) continue;
-
- Vector3 hp = col.ClosestPoint(center);
- Vector3 dir = (col.bounds.center - center).normalized;
-
- vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage))
- {
- sender = transform,
- hitPosition = hp,
- force = knockbackForce > 0f ? dir * knockbackForce : Vector3.zero
- };
-
- bool dealt = false;
-
- var receiver = col.GetComponent() ?? col.GetComponentInParent();
- if (receiver != null) { receiver.TakeDamage(damageInfo); dealt = true; }
-
- if (!dealt)
- {
- var health = col.GetComponent() ?? col.GetComponentInParent();
- if (health != null) { health.TakeDamage(damageInfo); dealt = true; }
- }
-
- if (!dealt)
- {
- var tpc = col.GetComponent() ?? col.GetComponentInParent();
- if (tpc != null)
- {
- if (tpc is Beyond.bThirdPersonController beyond)
- {
- if (!beyond.GodMode && !beyond.isImmortal) { tpc.TakeDamage(damageInfo); dealt = true; }
- }
- else { tpc.TakeDamage(damageInfo); dealt = true; }
- }
- }
+ rb.AddForce(hitDirection * knockbackForce, ForceMode.Impulse);
}
- // Consider AOE as the final damage application for this projectile
- hasDealtDamage = true;
- }
-
- ///
- /// Gets the closest point on a collider to this projectile's position.
- ///
- private Vector3 GetClosestPointOnCollider(Collider col)
- {
- return col.ClosestPoint(transform.position);
- }
-
- ///
- /// Computes a hit direction from projectile toward a collider's center; falls back to current velocity.
- ///
- private Vector3 GetHitDirection(Collider col)
- {
- Vector3 dir = (col.bounds.center - transform.position).normalized;
- return dir.sqrMagnitude > 0.0001f ? dir : SafeNormal(velocity);
+ if (enableDebug && damageDealt)
+ {
+ Debug.Log($"[MeteorProjectile] Dealt {damage} damage to {targetCollider.name}");
+ }
}
#endregion Impact & Damage
- #region Pooling
+ #region Helpers
- ///
- /// Returns this projectile to the LeanPool.
- ///
- private void Despawn()
+ private Vector3 SnapToGround(Vector3 point)
{
- if (enableDebug) Debug.Log("[Meteor] Despawn via LeanPool");
- LeanPool.Despawn(gameObject);
+ 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 Pooling
+ #endregion Helpers
#if UNITY_EDITOR
- ///
- /// Editor-only gizmos to visualize entry direction and impact point when selected.
- ///
private void OnDrawGizmosSelected()
{
- Gizmos.color = Color.cyan;
- Gizmos.DrawWireSphere(transform.position, 0.25f);
-
+ // Draw collision sphere
Gizmos.color = Color.red;
- Gizmos.DrawLine(transform.position, transform.position + SafeNormal(velocity.sqrMagnitude > 0 ? velocity : velocityDir) * 3f);
+ Gizmos.DrawWireSphere(transform.position, collisionRadius);
- Gizmos.color = Color.yellow;
- Gizmos.DrawWireSphere(impactPoint, 0.35f);
+ // 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
-
- #region Validation
-
- ///
- /// Clamps and validates configuration values in the inspector.
- ///
- private void OnValidate()
- {
- speed = Mathf.Max(0f, speed);
- maxLifeTime = Mathf.Max(0.01f, maxLifeTime);
- explosionRadius = Mathf.Max(0f, explosionRadius);
- castRadius = Mathf.Clamp(castRadius, 0.01f, 2f);
- entryAngleDownFromHorizontal = Mathf.Clamp(entryAngleDownFromHorizontal, 0f, 89f);
- azimuthAimWeight = Mathf.Clamp01(azimuthAimWeight);
- }
-
- #endregion Validation
}
+
+ public sealed class ReadOnlyInInspectorAttribute : PropertyAttribute
+ { }
}
\ No newline at end of file
diff --git a/Assets/AI/Demon/SA_CallMeteor.cs b/Assets/AI/Demon/SA_CallMeteor.cs
index 6f0d3decb..dc397bd07 100644
--- a/Assets/AI/Demon/SA_CallMeteor.cs
+++ b/Assets/AI/Demon/SA_CallMeteor.cs
@@ -1,15 +1,12 @@
using Invector.vCharacterController.AI.FSMBehaviour;
using Lean.Pool;
-using System.Collections;
using UnityEngine;
namespace DemonBoss.Magic
{
///
- /// FSM Action: Boss calls down a meteor.
- /// Shows a decal at the player's position, locks an impact point on ground,
- /// then spawns the MeteorProjectile prefab above that point after a delay.
- /// Cancels cleanly on state exit.
+ /// Spawns a meteor behind the BOSS and launches it toward the player's position
+ /// Similar mechanics to FireballProjectile but coming from above
///
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Call Meteor")]
public class SA_CallMeteor : vStateAction
@@ -21,124 +18,88 @@ namespace DemonBoss.Magic
[Tooltip("Prefab with MeteorProjectile component")]
public GameObject meteorPrefab;
- [Tooltip("Visual decal prefab marking impact point")]
- public GameObject decalPrefab;
+ [Tooltip("Distance behind the BOSS to spawn meteor (meters)")]
+ public float behindBossDistance = 3f;
- [Tooltip("Height above ground at which meteor spawns")]
- public float spawnHeight = 40f;
+ [Tooltip("Height above the BOSS to spawn meteor (meters)")]
+ public float aboveBossHeight = 8f;
- [Tooltip("Delay before meteor spawns after decal")]
- public float castDelay = 1.5f;
+ [Tooltip("Delay before meteor spawns (wind-up)")]
+ public float castDelay = 0.4f;
- [Header("Ground")]
- [Tooltip("Layer mask for ground raycast")]
- public LayerMask groundMask = -1;
+ [Header("Targeting")]
+ [Tooltip("Tag used to find the target (usually Player)")]
+ public string targetTag = "Player";
[Header("Debug")]
public bool enableDebug = false;
- private Transform player;
- private GameObject spawnedDecal;
- private Vector3 impactPoint;
+ private Transform _boss;
+ private Transform _target;
- private Coroutine _spawnRoutine;
- private CoroutineRunner _runner;
-
- ///
- /// Entry point for the FSM action, delegates to OnEnter/OnExit depending on execution type.
- ///
- public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
+ public override void DoAction(vIFSMBehaviourController fsm, vFSMComponentExecutionType execType = vFSMComponentExecutionType.OnStateUpdate)
{
- if (executionType == vFSMComponentExecutionType.OnStateEnter)
+ if (execType == vFSMComponentExecutionType.OnStateEnter)
+ {
OnEnter(fsm);
-
- if (executionType == vFSMComponentExecutionType.OnStateExit)
- OnExit();
+ }
}
- ///
- /// Acquires the player, locks the impact point on the ground, shows the decal,
- /// and starts the delayed meteor spawn coroutine.
- ///
private void OnEnter(vIFSMBehaviourController fsm)
{
- player = GameObject.FindGameObjectWithTag("Player")?.transform;
- if (player == null)
+ _boss = fsm.transform;
+ _target = GameObject.FindGameObjectWithTag(targetTag)?.transform;
+
+ if (_target == null)
{
- if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No player found!");
+ if (enableDebug) Debug.LogWarning("[SA_CallMeteor] No target found – abort");
return;
}
- // Raycast down from the player to lock the impact point
- Vector3 rayStart = player.position + Vector3.up * 5f;
- if (Physics.Raycast(rayStart, Vector3.down, out RaycastHit hit, 100f, groundMask, QueryTriggerInteraction.Ignore))
- impactPoint = hit.point;
- else
- impactPoint = player.position;
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Boss: {_boss.name}, Target: {_target.name}");
- // Spawn decal
- if (decalPrefab != null)
- spawnedDecal = LeanPool.Spawn(decalPrefab, impactPoint, Quaternion.identity);
-
- // Get or add a dedicated runner for coroutines
- var hostGO = fsm.transform.gameObject;
- _runner = hostGO.GetComponent();
- if (_runner == null) _runner = hostGO.AddComponent();
-
- // Start delayed spawn
- _spawnRoutine = _runner.StartCoroutine(SpawnMeteorAfterDelay());
- }
-
- ///
- /// Cancels the pending spawn and cleans up the decal when exiting the state.
- ///
- private void OnExit()
- {
- if (_runner != null && _spawnRoutine != null)
- {
- _runner.StopCoroutine(_spawnRoutine);
- _spawnRoutine = null;
- }
-
- if (spawnedDecal != null)
- {
- LeanPool.Despawn(spawnedDecal);
- spawnedDecal = null;
- }
- }
-
- ///
- /// Waits for the configured cast delay and then spawns the meteor.
- ///
- private IEnumerator SpawnMeteorAfterDelay()
- {
- yield return new WaitForSeconds(castDelay);
SpawnMeteor();
}
- ///
- /// Spawns the meteor prefab above the locked impact point and cleans up the decal.
- ///
private void SpawnMeteor()
{
- if (meteorPrefab == null) return;
-
- Vector3 spawnPos = impactPoint + Vector3.up * spawnHeight;
- LeanPool.Spawn(meteorPrefab, spawnPos, Quaternion.identity);
-
- if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor spawned at {spawnPos}, impact={impactPoint}");
-
- if (spawnedDecal != null)
+ if (meteorPrefab == null)
{
- LeanPool.Despawn(spawnedDecal);
- spawnedDecal = null;
+ if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing meteorPrefab");
+ return;
+ }
+
+ if (_boss == null || _target == null)
+ {
+ if (enableDebug) Debug.LogError("[SA_CallMeteor] Missing boss or target reference");
+ 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;
+
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Spawning meteor at: {spawnPos}");
+
+ // Spawn the meteor
+ var meteorGO = LeanPool.Spawn(meteorPrefab, spawnPos, Quaternion.identity);
+
+ // Configure the projectile to target the player
+ var meteorScript = meteorGO.GetComponent();
+ if (meteorScript != null)
+ {
+ // Set it to target the player's current position
+ meteorScript.useOverrideImpactPoint = true;
+ meteorScript.overrideImpactPoint = _target.position;
+ meteorScript.snapImpactToGround = true;
+
+ if (enableDebug) Debug.Log($"[SA_CallMeteor] Meteor configured to target: {_target.position}");
+ }
+ else
+ {
+ if (enableDebug) Debug.LogError("[SA_CallMeteor] Meteor prefab missing MeteorProjectile component!");
}
}
}
-
- ///
- /// Lightweight helper component dedicated to running coroutines for ScriptableObject actions.
- ///
- public sealed class CoroutineRunner : MonoBehaviour
- { }
}
\ No newline at end of file
diff --git a/Assets/AI/Demon/SA_SpawnTurretSmart.cs b/Assets/AI/Demon/SA_SpawnTurretSmart.cs
index 54dadff32..c00440bc0 100644
--- a/Assets/AI/Demon/SA_SpawnTurretSmart.cs
+++ b/Assets/AI/Demon/SA_SpawnTurretSmart.cs
@@ -5,140 +5,97 @@ using UnityEngine;
namespace DemonBoss.Magic
{
///
- /// StateAction for intelligent crystal turret spawning
- /// Searches for optimal position in 2-6m ring from boss, prefers "behind boss" relative to player
+ /// Spawns exactly 3 crystal turrets around the opponent.
+ /// Now with per-spawn randomness: global rotation offset and per-turret angle/radius jitter.
+ /// Validates positions (ground/obstacles) and enforces minimum separation.
///
- [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn Turret Smart")]
+ [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn 3 Turrets Radial")]
public class SA_SpawnTurretSmart : vStateAction
{
public override string categoryName => "DemonBoss/Magic";
- public override string defaultName => "Spawn Turret Smart";
+ public override string defaultName => "Spawn 3 Turrets Radial";
[Header("Turret Configuration")]
- [Tooltip("Crystal prefab with CrystalShooterAI component")]
public GameObject crystalPrefab;
- [Tooltip("Minimum distance from boss for crystal spawn")]
public float minSpawnDistance = 2f;
-
- [Tooltip("Maximum distance from boss for crystal spawn")]
public float maxSpawnDistance = 6f;
-
- [Tooltip("Collision check radius when choosing position")]
public float obstacleCheckRadius = 1f;
-
- [Tooltip("Height above ground for raycast ground checking")]
public float groundCheckHeight = 2f;
-
- [Tooltip("Layer mask for obstacles")]
public LayerMask obstacleLayerMask = -1;
-
- [Tooltip("Layer mask for ground")]
public LayerMask groundLayerMask = -1;
-
- [Tooltip("Animator bool parameter name for blocking state")]
public string animatorBlockingBool = "IsBlocking";
- [Header("Smart Positioning")]
- [Tooltip("Preference multiplier for positions behind boss (relative to player)")]
- public float backPreferenceMultiplier = 2f;
+ [Header("Placement Search (Validation)")]
+ public int perTurretAdjustmentTries = 10;
- [Tooltip("Number of attempts to find valid position")]
- public int maxSpawnAttempts = 12;
+ public float maxAngleAdjust = 25f;
+ public float maxRadiusAdjust = 1.0f;
+ public float minSeparationBetweenTurrets = 1.5f;
+
+ [Header("Randomization (Formation)")]
+ [Tooltip("Random global rotation offset (degrees) applied to the 120° spokes.")]
+ public bool globalStartAngleRandom = true;
+
+ [Tooltip("Per-turret angle jitter around the spoke (degrees). 0 = disabled.")]
+ public float perTurretAngleJitter = 10f;
+
+ [Tooltip("Per-turret radius jitter (meters). 0 = disabled.")]
+ public float perTurretRadiusJitter = 0.75f;
[Header("Debug")]
- [Tooltip("Enable debug logging")]
public bool enableDebug = false;
- [Tooltip("Show gizmos in Scene View")]
public bool showGizmos = true;
- private GameObject spawnedCrystal;
private Animator npcAnimator;
private Transform npcTransform;
private Transform playerTransform;
- ///
- /// Main action execution method called by FSM
- ///
+ private readonly System.Collections.Generic.List _lastPlanned = new System.Collections.Generic.List(3);
+
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
{
- if (executionType == vFSMComponentExecutionType.OnStateEnter)
- {
- OnStateEnter(fsmBehaviour);
- }
- else if (executionType == vFSMComponentExecutionType.OnStateExit)
- {
- OnStateExit(fsmBehaviour);
- }
+ if (executionType == vFSMComponentExecutionType.OnStateEnter) OnStateEnter(fsmBehaviour);
+ else if (executionType == vFSMComponentExecutionType.OnStateExit) OnStateExit(fsmBehaviour);
}
- ///
- /// Called when entering state - intelligently spawns crystal
- ///
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
{
- if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Starting intelligent crystal spawn");
-
- // Store NPC references
npcTransform = fsmBehaviour.transform;
npcAnimator = npcTransform.GetComponent();
FindPlayer(fsmBehaviour);
if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
- {
npcAnimator.SetBool(animatorBlockingBool, true);
- if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set bool: {animatorBlockingBool} = true");
- }
- SpawnCrystalSmart(fsmBehaviour);
+ SpawnThreeTurretsRadial(fsmBehaviour);
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f);
}
- ///
- /// Called when exiting state - cleanup
- ///
private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
{
- if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Exiting turret spawn state");
-
if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
- {
npcAnimator.SetBool(animatorBlockingBool, false);
- if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set bool: {animatorBlockingBool} = false");
- }
}
- ///
- /// Finds player transform
- ///
private void FindPlayer(vIFSMBehaviourController fsmBehaviour)
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
playerTransform = player.transform;
- if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found by tag");
return;
}
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
if (aiController != null && aiController.currentTarget != null)
- {
playerTransform = aiController.currentTarget.transform;
- if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found through AI target");
- return;
- }
-
- if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Player not found!");
}
- ///
- /// Intelligently spawns crystal in optimal position
- ///
- private void SpawnCrystalSmart(vIFSMBehaviourController fsmBehaviour)
+ private void SpawnThreeTurretsRadial(vIFSMBehaviourController fsmBehaviour)
{
if (crystalPrefab == null)
{
@@ -146,63 +103,85 @@ namespace DemonBoss.Magic
return;
}
- Vector3 bestPosition = Vector3.zero;
- bool foundValidPosition = false;
- float bestScore = float.MinValue;
+ _lastPlanned.Clear();
- Vector3 bossPos = npcTransform.position;
- Vector3 playerDirection = Vector3.zero;
+ Vector3 center = playerTransform != null ? playerTransform.position : npcTransform.position;
+ float baseRadius = Mathf.Clamp((minSpawnDistance + maxSpawnDistance) * 0.5f, minSpawnDistance, maxSpawnDistance);
- if (playerTransform != null)
+ Vector3 refDir = Vector3.forward;
+ if (playerTransform != null && npcTransform != null)
{
- playerDirection = (playerTransform.position - bossPos).normalized;
+ Vector3 d = (npcTransform.position - center); d.y = 0f;
+ if (d.sqrMagnitude > 0.0001f) refDir = d.normalized;
+ }
+ else if (npcTransform != null)
+ {
+ refDir = npcTransform.forward;
}
- for (int i = 0; i < maxSpawnAttempts; i++)
+ float globalOffset = globalStartAngleRandom ? Random.Range(0f, 360f) : 0f;
+
+ const int turretCount = 3;
+ for (int i = 0; i < turretCount; i++)
{
- float angle = (360f / maxSpawnAttempts) * i + Random.Range(-15f, 15f);
- Vector3 direction = new Vector3(Mathf.Cos(angle * Mathf.Deg2Rad), 0, Mathf.Sin(angle * Mathf.Deg2Rad));
+ float baseAngle = globalOffset + i * 120f;
+ float angle = baseAngle + (perTurretAngleJitter > 0f ? Random.Range(-perTurretAngleJitter, perTurretAngleJitter) : 0f);
- float distance = Random.Range(minSpawnDistance, maxSpawnDistance);
- Vector3 testPosition = bossPos + direction * distance;
+ float radius = baseRadius + (perTurretRadiusJitter > 0f ? Random.Range(-perTurretRadiusJitter, perTurretRadiusJitter) : 0f);
+ radius = Mathf.Clamp(radius, minSpawnDistance, maxSpawnDistance);
- if (IsPositionValid(testPosition, out Vector3 groundPosition))
+ Vector3 ideal = center + Quaternion.Euler(0f, angle, 0f) * refDir * radius;
+
+ Vector3? chosen = IsPositionValidAndSeparated(ideal)
+ ? (Vector3?)ideal
+ : FindValidPositionAroundSpoke(center, refDir, angle, radius);
+
+ if (!chosen.HasValue)
{
- float score = EvaluatePosition(groundPosition, playerDirection, direction, bossPos);
-
- if (score > bestScore)
- {
- bestScore = score;
- bestPosition = groundPosition;
- foundValidPosition = true;
- }
+ Vector3 rough = center + Quaternion.Euler(0f, angle, 0f) * refDir * radius;
+ chosen = EnforceSeparationFallback(rough, center);
}
- }
- if (foundValidPosition)
- {
- SpawnCrystal(bestPosition, fsmBehaviour);
- if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Crystal spawned at position: {bestPosition} (score: {bestScore:F2})");
- }
- else
- {
- Vector3 fallbackPos = bossPos + npcTransform.forward * minSpawnDistance;
- SpawnCrystal(fallbackPos, fsmBehaviour);
- if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Using fallback position");
+ Vector3 spawnPos = chosen.Value;
+ _lastPlanned.Add(spawnPos);
+ SpawnCrystal(spawnPos, fsmBehaviour);
}
}
- ///
- /// Checks if position is valid (no obstacles, has ground)
- ///
+ private Vector3? FindValidPositionAroundSpoke(Vector3 center, Vector3 refDir, float baseAngleDeg, float desiredRadius)
+ {
+ Vector3 ideal = center + Quaternion.Euler(0f, baseAngleDeg, 0f) * refDir * desiredRadius;
+ if (IsPositionValidAndSeparated(ideal)) return ideal;
+
+ for (int t = 0; t < perTurretAdjustmentTries; t++)
+ {
+ float ang = baseAngleDeg + Random.Range(-maxAngleAdjust, maxAngleAdjust);
+ float rad = Mathf.Clamp(desiredRadius + Random.Range(-maxRadiusAdjust, maxRadiusAdjust), minSpawnDistance, maxSpawnDistance);
+ Vector3 cand = center + Quaternion.Euler(0f, ang, 0f) * refDir * rad;
+
+ if (IsPositionValidAndSeparated(cand)) return cand;
+ }
+ return null;
+ }
+
+ private bool IsPositionValidAndSeparated(Vector3 position)
+ {
+ if (!IsPositionValid(position, out Vector3 grounded)) return false;
+
+ for (int i = 0; i < _lastPlanned.Count; i++)
+ {
+ if (Vector3.Distance(_lastPlanned[i], grounded) < Mathf.Max(minSeparationBetweenTurrets, obstacleCheckRadius * 2f))
+ return false;
+ }
+ return true;
+ }
+
private bool IsPositionValid(Vector3 position, out Vector3 groundPosition)
{
groundPosition = position;
if (Physics.CheckSphere(position + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
- {
return false;
- }
Ray groundRay = new Ray(position + Vector3.up * groundCheckHeight, Vector3.down);
if (Physics.Raycast(groundRay, out RaycastHit hit, groundCheckHeight + 2f, groundLayerMask))
@@ -210,109 +189,82 @@ namespace DemonBoss.Magic
groundPosition = hit.point;
if (Physics.CheckSphere(groundPosition + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
- {
return false;
- }
return true;
}
-
return false;
}
- ///
- /// Evaluates position quality (higher score = better position)
- ///
- private float EvaluatePosition(Vector3 position, Vector3 playerDirection, Vector3 positionDirection, Vector3 bossPos)
+ private Vector3 EnforceSeparationFallback(Vector3 desired, Vector3 center)
{
- float score = 0f;
-
- if (playerTransform != null && playerDirection != Vector3.zero)
+ Vector3 candidate = desired;
+ float step = 0.5f;
+ for (int i = 0; i < 10; i++)
{
- float angleToPlayer = Vector3.Angle(-playerDirection, positionDirection);
+ bool ok = true;
+ for (int k = 0; k < _lastPlanned.Count; k++)
+ {
+ if (Vector3.Distance(_lastPlanned[k], candidate) < Mathf.Max(minSeparationBetweenTurrets, obstacleCheckRadius * 2f))
+ { ok = false; break; }
+ }
+ if (ok) return candidate;
- // The smaller the angle (closer to "behind"), the better the score
- float backScore = (180f - angleToPlayer) / 180f;
- score += backScore * backPreferenceMultiplier;
+ Vector3 dir = (candidate - center); dir.y = 0f;
+ if (dir.sqrMagnitude < 0.0001f) dir = Vector3.right;
+ candidate = center + dir.normalized * (dir.magnitude + step);
}
-
- float distance = Vector3.Distance(position, bossPos);
- float optimalDistance = (minSpawnDistance + maxSpawnDistance) * 0.5f;
- float distanceScore = 1f - Mathf.Abs(distance - optimalDistance) / maxSpawnDistance;
- score += distanceScore;
-
- score += Random.Range(-0.1f, 0.1f);
-
- return score;
+ return candidate;
}
- ///
- /// Spawns crystal at given position
- ///
private void SpawnCrystal(Vector3 position, vIFSMBehaviourController fsmBehaviour)
{
Quaternion rotation = Quaternion.identity;
- if (playerTransform != null)
+ Transform lookAt = playerTransform != null ? playerTransform : npcTransform;
+
+ if (lookAt != null)
{
- Vector3 lookDirection = (playerTransform.position - position).normalized;
+ Vector3 lookDirection = (lookAt.position - position).normalized;
lookDirection.y = 0;
if (lookDirection != Vector3.zero)
- {
rotation = Quaternion.LookRotation(lookDirection);
- }
}
- spawnedCrystal = LeanPool.Spawn(crystalPrefab, position, rotation);
+ var spawned = LeanPool.Spawn(crystalPrefab, position, rotation);
- var shooterAI = spawnedCrystal.GetComponent();
+ var shooterAI = spawned.GetComponent();
if (shooterAI == null)
{
Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!");
}
- else
+ else if (playerTransform != null)
{
- if (playerTransform != null)
- {
- shooterAI.SetTarget(playerTransform);
- }
+ shooterAI.SetTarget(playerTransform);
}
}
- ///
- /// Draws gizmos in Scene View for debugging
- ///
private void OnDrawGizmosSelected()
{
- if (!showGizmos) return;
+ if (!showGizmos || npcTransform == null) return;
- if (npcTransform != null)
- {
- Vector3 pos = npcTransform.position;
+ Vector3 c = playerTransform ? playerTransform.position : npcTransform.position;
- // Spawn ring
- Gizmos.color = Color.green;
- DrawWireCircle(pos, minSpawnDistance);
- Gizmos.color = Color.red;
- DrawWireCircle(pos, maxSpawnDistance);
+ Gizmos.color = Color.green;
+ DrawWireCircle(c, minSpawnDistance);
+ Gizmos.color = Color.red;
+ DrawWireCircle(c, maxSpawnDistance);
- // Obstacle check radius
- Gizmos.color = Color.yellow;
- Gizmos.DrawWireSphere(pos + Vector3.up * obstacleCheckRadius, obstacleCheckRadius);
- }
+ Gizmos.color = Color.cyan;
+ foreach (var p in _lastPlanned) Gizmos.DrawWireSphere(p + Vector3.up * 0.1f, 0.2f);
}
- ///
- /// Helper method for drawing circles
- ///
private void DrawWireCircle(Vector3 center, float radius)
{
int segments = 32;
- float angle = 0f;
Vector3 prevPoint = center + new Vector3(radius, 0, 0);
-
for (int i = 1; i <= segments; i++)
{
- angle = (float)i / segments * 360f * Mathf.Deg2Rad;
+ float angle = (float)i / segments * 360f * Mathf.Deg2Rad;
Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
Gizmos.DrawLine(prevPoint, newPoint);
prevPoint = newPoint;
diff --git a/Assets/AI/Demon/ShieldDamage.cs b/Assets/AI/Demon/ShieldDamage.cs
new file mode 100644
index 000000000..9d9b2398a
--- /dev/null
+++ b/Assets/AI/Demon/ShieldDamage.cs
@@ -0,0 +1,191 @@
+using Invector;
+using Invector.vCharacterController;
+using UnityEngine;
+
+namespace DemonBoss.Magic
+{
+ ///
+ /// Component that adds damage when the player gets too close to the shield.
+ /// Add to the shield object along with Sphere Collider (IsTrigger = true).
+ ///
+ 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();
+ if (audioSource == null)
+ {
+ audioSource = gameObject.AddComponent();
+ 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() ??
+ playerCollider.GetComponentInParent();
+
+ 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() ??
+ playerCollider.GetComponentInParent();
+
+ 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() ??
+ playerCollider.GetComponentInParent();
+
+ 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
+
+ ///
+ /// Draw damage range visualization in Scene View
+ ///
+ private void OnDrawGizmosSelected()
+ {
+ // Show damage range
+ Collider col = GetComponent();
+ 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
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/Demon/ShieldDamage.cs.meta b/Assets/AI/Demon/ShieldDamage.cs.meta
new file mode 100644
index 000000000..9eb45d81b
--- /dev/null
+++ b/Assets/AI/Demon/ShieldDamage.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3a4b288b220c78b4ca00e13cb8a72d6b
\ No newline at end of file
diff --git a/Assets/AI/Demon/Turet.prefab b/Assets/AI/Demon/Turet.prefab
index 8b5ee6dad..488995536 100644
--- a/Assets/AI/Demon/Turet.prefab
+++ b/Assets/AI/Demon/Turet.prefab
@@ -13,6 +13,9 @@ GameObject:
- component: {fileID: 2323612}
- component: {fileID: 13662188}
- component: {fileID: 7020133711031364094}
+ - component: {fileID: -8194063383422297065}
+ - component: {fileID: 1536958584606674588}
+ - component: {fileID: 8851143338676511289}
m_Layer: 26
m_Name: Turet
m_TagString: Untagged
@@ -104,7 +107,7 @@ CapsuleCollider:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
- m_IsTrigger: 0
+ m_IsTrigger: 1
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 2
@@ -128,19 +131,73 @@ MonoBehaviour:
fireballPrefab: {fileID: 1947871717301538, guid: 9591667a35466484096c6e63785e136c,
type: 3}
fireRate: 5
- maxShots: 3
+ maxShots: 10
despawnDelay: 10
- turnSpeed: 120
- idleSpinSpeed: 30
- aimTolerance: 5
+ initialStaggerRange: {x: 0, y: 0.6}
+ fireRateJitter: 0.5
+ aimJitterDegrees: 0
+ turnSpeed: 0
+ idleSpinSpeed: 0
+ aimTolerance: 360
autoFindPlayer: 1
playerTag: Player
maxShootingRange: 50
- useShootEffects: 0
+ useShootEffects: 1
muzzleFlashPrefab: {fileID: 0}
- shootSound: {fileID: 8300000, guid: d44a96f293b66b74ea13a2e6fcc6c8fa, type: 3}
- enableDebug: 0
+ shootSound: {fileID: 8300000, guid: f58578c3593cfcd40a4b21417e49421a, type: 3}
+ enableDebug: 1
showGizmos: 1
+--- !u!114 &-8194063383422297065
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 115880}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 06579ea47ceeddd42a05f7720c15b5de, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ maxHealth: 50
+ currentHealth: 0
+ destructionEffect: {fileID: 8876690725160639820, guid: c1b3da9fe585c44479dda2cd5c3a7b83,
+ type: 3}
+ destructionSound: {fileID: 8300000, guid: 3c1f8b87da670734991b73bf5c30f0af, type: 3}
+ hitSound: {fileID: 8300000, guid: 0af332725d9792840a4caa29e8991d08, type: 3}
+ damagedMaterial: {fileID: 2100000, guid: f94eea72c30ac2e45ab55894421ea48c, type: 2}
+ damagedThreshold: 0.5
+ enableDebug: 1
+--- !u!114 &1536958584606674588
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 115880}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 605ff11ee57b0c241a82c0c37c40c0bc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ openCloseEvents: 0
+ openCloseWindow: 0
+ selectedToolbar: 0
+ messagesListeners: []
+--- !u!114 &8851143338676511289
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 115880}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 312629d966d1a40e3874daa93364e1f3, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ targetIcon: {fileID: 8317937118480752073, guid: 82581d6c5dd5945b89394d83db5f4c8b,
+ type: 3}
--- !u!1 &6534933121460188189
GameObject:
m_ObjectHideFlags: 0
diff --git a/Assets/AI/FSM/FSM_Demon.asset b/Assets/AI/FSM/FSM_Demon.asset
index ad64ecdef..e3657fcb6 100644
--- a/Assets/AI/FSM/FSM_Demon.asset
+++ b/Assets/AI/FSM/FSM_Demon.asset
@@ -135,38 +135,6 @@ MonoBehaviour:
shieldDuration: 10
animatorBlockingBool: IsBlocking
enableDebug: 1
---- !u!114 &-7016508256595524775
-MonoBehaviour:
- m_ObjectHideFlags: 1
- 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: af82d1d082ce4b9478d420f8ca1e72c2, type: 3}
- m_Name: Check Cooldown Meteor
- m_EditorClassIdentifier:
- parentFSM: {fileID: 11400000}
- editingName: 0
- trueRect:
- serializedVersion: 2
- x: 0
- y: 0
- width: 10
- height: 10
- falseRect:
- serializedVersion: 2
- x: 0
- y: 0
- width: 10
- height: 10
- selectedTrue: 0
- selectedFalse: 0
- cooldownKey: Meteor
- cooldownTime: 75
- availableAtStart: 1
- enableDebug: 0
--- !u!114 &-6568372008305276654
MonoBehaviour:
m_ObjectHideFlags: 1
@@ -190,17 +158,17 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: -368
- y: 311
+ x: -35
+ y: 290
width: 150
height: 62
- positionRect: {x: -368, y: 311}
+ positionRect: {x: -35, y: 290}
rectWidth: 150
editingName: 1
nodeColor: {r: 0, g: 1, b: 1, a: 1}
resizeLeft: 0
resizeRight: 0
- inDrag: 1
+ inDrag: 0
resetCurrentDestination: 0
transitions:
- decisions:
@@ -221,14 +189,14 @@ MonoBehaviour:
parentState: {fileID: -6568372008305276654}
trueRect:
serializedVersion: 2
- x: -218
- y: 341
+ x: 115
+ y: 320
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -218
- y: 351
+ x: 115
+ y: 330
width: 10
height: 10
selectedTrue: 0
@@ -261,12 +229,10 @@ MonoBehaviour:
editingName: 0
meteorPrefab: {fileID: 1947871717301538, guid: f99aa3faf46a5f94985344f44aaf21aa,
type: 3}
- decalPrefab: {fileID: 0}
- spawnHeight: 40
+ behindBossDistance: 10
+ aboveBossHeight: 20
castDelay: 1.5
- groundMask:
- serializedVersion: 2
- m_Bits: 4294967295
+ targetTag: Player
enableDebug: 1
--- !u!114 &-6379838510941931433
MonoBehaviour:
@@ -321,17 +287,17 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: -328
- y: 466
+ x: 0
+ y: 445
width: 150
height: 106
- positionRect: {x: -328, y: 466}
+ positionRect: {x: 0, y: 445}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 0, b: 0, a: 1}
resizeLeft: 0
resizeRight: 0
- inDrag: 1
+ inDrag: 0
resetCurrentDestination: 0
transitions:
- decisions:
@@ -352,14 +318,14 @@ MonoBehaviour:
parentState: {fileID: -6144582714324757854}
trueRect:
serializedVersion: 2
- x: -178
- y: 496
+ x: 150
+ y: 475
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -178
- y: 506
+ x: 150
+ y: 485
width: 10
height: 10
selectedTrue: 0
@@ -380,14 +346,14 @@ MonoBehaviour:
parentState: {fileID: -6144582714324757854}
trueRect:
serializedVersion: 2
- x: -178
- y: 518
+ x: 150
+ y: 497
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -178
- y: 528
+ x: 150
+ y: 507
width: 10
height: 10
selectedTrue: 0
@@ -416,14 +382,14 @@ MonoBehaviour:
parentState: {fileID: -6144582714324757854}
trueRect:
serializedVersion: 2
- x: -178
- y: 540
+ x: 150
+ y: 519
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -178
- y: 550
+ x: 150
+ y: 529
width: 10
height: 10
selectedTrue: 0
@@ -574,17 +540,17 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: -48
- y: 316
+ x: 280
+ y: 295
width: 150
height: 62
- positionRect: {x: -48, y: 316}
+ positionRect: {x: 280, y: 295}
rectWidth: 150
editingName: 1
nodeColor: {r: 0.10323405, g: 1, b: 0, a: 1}
resizeLeft: 0
resizeRight: 0
- inDrag: 1
+ inDrag: 0
resetCurrentDestination: 1
transitions:
- decisions: []
@@ -597,14 +563,14 @@ MonoBehaviour:
parentState: {fileID: -3177478727897100882}
trueRect:
serializedVersion: 2
- x: 102
- y: 346
+ x: 430
+ y: 325
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 102
- y: 356
+ x: 430
+ y: 335
width: 10
height: 10
selectedTrue: 0
@@ -643,17 +609,17 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: 252
- y: 271
+ x: 580
+ y: 250
width: 150
height: 62
- positionRect: {x: 252, y: 271}
+ positionRect: {x: 580, y: 250}
rectWidth: 150
editingName: 1
nodeColor: {r: 0, g: 1, b: 0.004989147, a: 1}
resizeLeft: 0
resizeRight: 0
- inDrag: 1
+ inDrag: 0
resetCurrentDestination: 0
transitions:
- decisions:
@@ -670,14 +636,14 @@ MonoBehaviour:
parentState: {fileID: -2904979146780567904}
trueRect:
serializedVersion: 2
- x: 242
- y: 301
+ x: 570
+ y: 280
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 402
- y: 311
+ x: 730
+ y: 290
width: 10
height: 10
selectedTrue: 0
@@ -778,8 +744,13 @@ MonoBehaviour:
serializedVersion: 2
m_Bits: 4294967295
animatorBlockingBool: IsBlocking
- backPreferenceMultiplier: 2
- maxSpawnAttempts: 12
+ perTurretAdjustmentTries: 10
+ maxAngleAdjust: 25
+ maxRadiusAdjust: 1
+ minSeparationBetweenTurrets: 1.5
+ globalStartAngleRandom: 1
+ perTurretAngleJitter: 10
+ perTurretRadiusJitter: 0.75
enableDebug: 1
showGizmos: 1
--- !u!114 &-712571192746352845
@@ -805,17 +776,17 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: -48
- y: 256
+ x: 280
+ y: 235
width: 150
height: 30
- positionRect: {x: -48, y: 256}
+ positionRect: {x: 280, y: 235}
rectWidth: 150
editingName: 0
nodeColor: {r: 0, g: 1, b: 0, a: 1}
resizeLeft: 0
resizeRight: 0
- inDrag: 1
+ inDrag: 0
resetCurrentDestination: 0
transitions: []
actions: []
@@ -881,27 +852,27 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: -243
- y: 726
+ x: 85
+ y: 705
width: 150
height: 150
- positionRect: {x: -243, y: 726}
+ positionRect: {x: 85, y: 705}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 0.95132554, b: 0, a: 1}
resizeLeft: 0
resizeRight: 0
- inDrag: 1
+ inDrag: 0
resetCurrentDestination: 1
transitions:
- decisions:
- trueValue: 1
decision: {fileID: 4031404829621142413}
- isValid: 1
+ isValid: 0
validated: 0
- trueValue: 1
decision: {fileID: -2866484833343459521}
- isValid: 1
+ isValid: 0
validated: 0
trueState: {fileID: 2986668563461644515}
falseState: {fileID: 0}
@@ -912,14 +883,14 @@ MonoBehaviour:
parentState: {fileID: -312774025800194259}
trueRect:
serializedVersion: 2
- x: -93
- y: 756
+ x: 235
+ y: 735
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -93
- y: 766
+ x: 235
+ y: 745
width: 10
height: 10
selectedTrue: 0
@@ -933,7 +904,7 @@ MonoBehaviour:
- decisions:
- trueValue: 0
decision: {fileID: 4031404829621142413}
- isValid: 0
+ isValid: 1
validated: 0
trueState: {fileID: -2904979146780567904}
falseState: {fileID: 0}
@@ -944,14 +915,14 @@ MonoBehaviour:
parentState: {fileID: -312774025800194259}
trueRect:
serializedVersion: 2
- x: -93
- y: 778
+ x: 235
+ y: 757
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -93
- y: 788
+ x: 235
+ y: 767
width: 10
height: 10
selectedTrue: 0
@@ -965,16 +936,16 @@ MonoBehaviour:
- decisions:
- trueValue: 1
decision: {fileID: 4031404829621142413}
- isValid: 1
- validated: 0
- - trueValue: 0
- decision: {fileID: -2866484833343459521}
isValid: 0
validated: 0
- trueValue: 0
- decision: {fileID: 7927421991537792917}
+ decision: {fileID: -2866484833343459521}
isValid: 1
validated: 0
+ - trueValue: 0
+ decision: {fileID: 7927421991537792917}
+ isValid: 0
+ validated: 0
trueState: {fileID: -6144582714324757854}
falseState: {fileID: 0}
muteTrue: 0
@@ -984,14 +955,14 @@ MonoBehaviour:
parentState: {fileID: -312774025800194259}
trueRect:
serializedVersion: 2
- x: -253
- y: 800
+ x: 75
+ y: 779
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -93
- y: 810
+ x: 235
+ y: 789
width: 10
height: 10
selectedTrue: 0
@@ -1005,11 +976,11 @@ MonoBehaviour:
- decisions:
- trueValue: 1
decision: {fileID: -3690511210373239573}
- isValid: 1
+ isValid: 0
validated: 0
- trueValue: 0
decision: {fileID: 7927421991537792917}
- isValid: 1
+ isValid: 0
validated: 0
trueState: {fileID: 0}
falseState: {fileID: 0}
@@ -1020,14 +991,14 @@ MonoBehaviour:
parentState: {fileID: -312774025800194259}
trueRect:
serializedVersion: 2
- x: -93
- y: 822
+ x: 235
+ y: 801
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -93
- y: 832
+ x: 235
+ y: 811
width: 10
height: 10
selectedTrue: 0
@@ -1041,7 +1012,7 @@ MonoBehaviour:
- decisions:
- trueValue: 1
decision: {fileID: 7927421991537792917}
- isValid: 0
+ isValid: 1
validated: 0
trueState: {fileID: -2904979146780567904}
falseState: {fileID: 0}
@@ -1052,14 +1023,14 @@ MonoBehaviour:
parentState: {fileID: -312774025800194259}
trueRect:
serializedVersion: 2
- x: -93
- y: 844
+ x: 235
+ y: 823
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: -93
- y: 854
+ x: 235
+ y: 833
width: 10
height: 10
selectedTrue: 0
@@ -1106,7 +1077,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: a5fc604039227434d8b4e63ebc5e74a5, type: 3}
m_Name: FSM_Demon
m_EditorClassIdentifier:
- selectedNode: {fileID: 4162026404432437805}
+ selectedNode: {fileID: 2986668563461644515}
wantConnection: 0
connectionNode: {fileID: 0}
showProperties: 1
@@ -1123,7 +1094,7 @@ MonoBehaviour:
- {fileID: 9112689765763526057}
- {fileID: 766956384951898899}
- {fileID: 4162026404432437805}
- panOffset: {x: -410, y: 290}
+ panOffset: {x: -795, y: 390}
overNode: 0
actions:
- {fileID: 0}
@@ -1207,17 +1178,17 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: 772
- y: 956
+ x: 1100
+ y: 935
width: 150
height: 62
- positionRect: {x: 772, y: 956}
+ positionRect: {x: 1100, y: 935}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 1, b: 1, a: 1}
resizeLeft: 0
resizeRight: 0
- inDrag: 1
+ inDrag: 0
resetCurrentDestination: 1
transitions:
- decisions: []
@@ -1230,14 +1201,14 @@ MonoBehaviour:
parentState: {fileID: 762670965814380212}
trueRect:
serializedVersion: 2
- x: 762
- y: 986
+ x: 1090
+ y: 965
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 922
- y: 996
+ x: 1250
+ y: 975
width: 10
height: 10
selectedTrue: 0
@@ -1277,11 +1248,11 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: 840
- y: 410
+ x: 1165
+ y: 385
width: 150
height: 62
- positionRect: {x: 840, y: 410}
+ positionRect: {x: 1165, y: 385}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 1, b: 1, a: 1}
@@ -1300,14 +1271,14 @@ MonoBehaviour:
parentState: {fileID: 766956384951898899}
trueRect:
serializedVersion: 2
- x: 830
- y: 440
+ x: 1155
+ y: 415
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 990
- y: 450
+ x: 1315
+ y: 425
width: 10
height: 10
selectedTrue: 0
@@ -1396,17 +1367,17 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: 562
- y: 956
+ x: 890
+ y: 935
width: 150
height: 62
- positionRect: {x: 562, y: 956}
+ positionRect: {x: 890, y: 935}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 1, b: 1, a: 1}
resizeLeft: 0
resizeRight: 0
- inDrag: 1
+ inDrag: 0
resetCurrentDestination: 1
transitions:
- decisions: []
@@ -1419,14 +1390,14 @@ MonoBehaviour:
parentState: {fileID: 2691300596403639167}
trueRect:
serializedVersion: 2
- x: 552
- y: 986
+ x: 880
+ y: 965
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 712
- y: 996
+ x: 1040
+ y: 975
width: 10
height: 10
selectedTrue: 0
@@ -1456,33 +1427,33 @@ MonoBehaviour:
m_Name: Combat
m_EditorClassIdentifier:
description: FSM State
- selectedDecisionIndex: 0
+ selectedDecisionIndex: 4
canRemove: 1
canTranstTo: 1
canSetAsDefault: 1
canEditName: 1
canEditColor: 1
isOpen: 1
- isSelected: 0
+ isSelected: 1
nodeRect:
serializedVersion: 2
- x: 307
- y: 676
+ x: 635
+ y: 655
width: 150
height: 150
- positionRect: {x: 307, y: 676}
+ positionRect: {x: 635, y: 655}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 0, b: 0, a: 1}
resizeLeft: 0
resizeRight: 0
- inDrag: 1
+ inDrag: 0
resetCurrentDestination: 1
transitions:
- decisions:
- trueValue: 1
decision: {fileID: 7927421991537792917}
- isValid: 1
+ isValid: 0
validated: 0
trueState: {fileID: -2904979146780567904}
falseState: {fileID: 0}
@@ -1493,14 +1464,14 @@ MonoBehaviour:
parentState: {fileID: 2986668563461644515}
trueRect:
serializedVersion: 2
- x: 297
- y: 706
+ x: 625
+ y: 685
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 457
- y: 716
+ x: 785
+ y: 695
width: 10
height: 10
selectedTrue: 0
@@ -1525,14 +1496,14 @@ MonoBehaviour:
parentState: {fileID: 2986668563461644515}
trueRect:
serializedVersion: 2
- x: 297
- y: 728
+ x: 625
+ y: 707
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 457
- y: 738
+ x: 785
+ y: 717
width: 10
height: 10
selectedTrue: 0
@@ -1561,14 +1532,14 @@ MonoBehaviour:
parentState: {fileID: 2986668563461644515}
trueRect:
serializedVersion: 2
- x: 457
- y: 750
+ x: 785
+ y: 729
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 457
- y: 760
+ x: 785
+ y: 739
width: 10
height: 10
selectedTrue: 0
@@ -1586,7 +1557,7 @@ MonoBehaviour:
validated: 0
- trueValue: 1
decision: {fileID: 8113515040269600600}
- isValid: 1
+ isValid: 0
validated: 0
trueState: {fileID: 9112689765763526057}
falseState: {fileID: 0}
@@ -1597,14 +1568,14 @@ MonoBehaviour:
parentState: {fileID: 2986668563461644515}
trueRect:
serializedVersion: 2
- x: 457
- y: 772
+ x: 785
+ y: 751
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 457
- y: 782
+ x: 785
+ y: 761
width: 10
height: 10
selectedTrue: 0
@@ -1618,11 +1589,11 @@ MonoBehaviour:
- decisions:
- trueValue: 0
decision: {fileID: -6379838510941931433}
- isValid: 0
+ isValid: 1
validated: 0
- trueValue: 1
- decision: {fileID: -7016508256595524775}
- isValid: 1
+ decision: {fileID: 2998305265418220943}
+ isValid: 0
validated: 0
trueState: {fileID: 4162026404432437805}
falseState: {fileID: 0}
@@ -1633,17 +1604,17 @@ MonoBehaviour:
parentState: {fileID: 2986668563461644515}
trueRect:
serializedVersion: 2
- x: 457
- y: 794
+ x: 785
+ y: 773
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 457
- y: 804
+ x: 785
+ y: 783
width: 10
height: 10
- selectedTrue: 0
+ selectedTrue: 1
selectedFalse: 0
trueSideRight: 1
falseSideRight: 1
@@ -1657,6 +1628,38 @@ MonoBehaviour:
useDecisions: 1
parentGraph: {fileID: 11400000}
defaultTransition: {fileID: 0}
+--- !u!114 &2998305265418220943
+MonoBehaviour:
+ m_ObjectHideFlags: 1
+ 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: af82d1d082ce4b9478d420f8ca1e72c2, type: 3}
+ m_Name: Check Cooldown Meteor
+ m_EditorClassIdentifier:
+ parentFSM: {fileID: 11400000}
+ editingName: 0
+ trueRect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 10
+ height: 10
+ falseRect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 10
+ height: 10
+ selectedTrue: 0
+ selectedFalse: 0
+ cooldownKey: Meteor
+ cooldownTime: 100
+ availableAtStart: 1
+ enableDebug: 1
--- !u!114 &4031404829621142413
MonoBehaviour:
m_ObjectHideFlags: 1
@@ -1705,14 +1708,14 @@ MonoBehaviour:
canEditName: 1
canEditColor: 1
isOpen: 0
- isSelected: 1
+ isSelected: 0
nodeRect:
serializedVersion: 2
- x: 840
- y: 625
+ x: 1160
+ y: 600
width: 150
height: 30
- positionRect: {x: 840, y: 625}
+ positionRect: {x: 1160, y: 600}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 1, b: 1, a: 1}
@@ -1731,14 +1734,14 @@ MonoBehaviour:
parentState: {fileID: 4162026404432437805}
trueRect:
serializedVersion: 2
- x: 915
- y: 640
+ x: 1235
+ y: 615
width: 0
height: 0
falseRect:
serializedVersion: 2
- x: 915
- y: 640
+ x: 1235
+ y: 615
width: 0
height: 0
selectedTrue: 0
@@ -1862,7 +1865,7 @@ MonoBehaviour:
cooldownKey: Shield
cooldownTime: 75
availableAtStart: 1
- enableDebug: 0
+ enableDebug: 1
--- !u!114 &8860036500635384459
MonoBehaviour:
m_ObjectHideFlags: 1
@@ -1904,11 +1907,11 @@ MonoBehaviour:
isSelected: 0
nodeRect:
serializedVersion: 2
- x: 845
- y: 230
+ x: 1170
+ y: 205
width: 150
height: 62
- positionRect: {x: 845, y: 230}
+ positionRect: {x: 1170, y: 205}
rectWidth: 150
editingName: 1
nodeColor: {r: 1, g: 1, b: 1, a: 1}
@@ -1927,14 +1930,14 @@ MonoBehaviour:
parentState: {fileID: 9112689765763526057}
trueRect:
serializedVersion: 2
- x: 835
- y: 260
+ x: 1160
+ y: 235
width: 10
height: 10
falseRect:
serializedVersion: 2
- x: 995
- y: 270
+ x: 1320
+ y: 245
width: 10
height: 10
selectedTrue: 0