Merge branch 'NewStory' of http://185.56.209.148/beyond/beyond into NewStory

This commit is contained in:
2025-08-25 13:19:48 +02:00
22 changed files with 28945 additions and 18924 deletions

View File

@@ -1,59 +1,55 @@
using Lean.Pool; using Lean.Pool;
using System.Collections; using System.Collections;
using UnityEngine; using UnityEngine;
namespace DemonBoss.Magic namespace DemonBoss.Magic
{ {
/// <summary>
/// AI component for crystal turret spawned by boss
/// Rotates towards player and shoots fireballs for specified time
/// </summary>
public class CrystalShooterAI : MonoBehaviour public class CrystalShooterAI : MonoBehaviour
{ {
[Header("Shooting Configuration")] [Header("Shooting Configuration")]
[Tooltip("Transform point from which projectiles are fired")] [Tooltip("Transform point from which projectiles are fired")]
public Transform muzzle; public Transform muzzle;
[Tooltip("Fireball prefab")] [Tooltip("Fireball prefab (projectile with its own targeting logic)")]
public GameObject fireballPrefab; public GameObject fireballPrefab;
[Tooltip("Fireball speed in m/s")] [Tooltip("Seconds between shots")]
public float fireballSpeed = 28f;
[Tooltip("Time between shots in seconds")]
public float fireRate = 0.7f; public float fireRate = 0.7f;
[Tooltip("Maximum number of shots before auto-despawn")] [Tooltip("Maximum number of shots before auto-despawn")]
public int maxShots = 10; public int maxShots = 10;
[Tooltip("Wait time after shooting before despawn")] [Tooltip("Wait time after last shot before despawn")]
public float despawnDelay = 3f; public float despawnDelay = 3f;
[Header("Rotation Configuration")] [Header("Rotation Configuration")]
[Tooltip("Target rotation speed in degrees/s")] [Tooltip("Yaw rotation speed in degrees per second")]
public float turnSpeed = 120f; public float turnSpeed = 120f;
[Tooltip("Idle spin speed when no target (degrees/s, 0 = disabled)")] [Tooltip("Idle spin speed when no target (degrees/s, 0 = disabled)")]
public float idleSpinSpeed = 30f; public float idleSpinSpeed = 30f;
[Tooltip("Aiming accuracy in degrees (smaller value = more accurate)")] [Tooltip("Aiming accuracy in degrees (smaller = stricter)")]
public float aimTolerance = 5f; public float aimTolerance = 5f;
[Header("Targeting")] [Header("Targeting (Turret-Side Only)")]
[Tooltip("Automatically find player on start")] [Tooltip("Auto-find player on start by tag")]
public bool autoFindPlayer = true; public bool autoFindPlayer = true;
[Tooltip("Player tag to search for")] [Tooltip("Player tag to search for")]
public string playerTag = "Player"; public string playerTag = "Player";
[Tooltip("Maximum shooting range")] [Tooltip("Max range for allowing shots")]
public float maxShootingRange = 50f; public float maxShootingRange = 50f;
[Header("Effects")] [Header("Effects")]
[Tooltip("Particle effect at shot")] [Tooltip("Enable or disable muzzle flash & sound effects when firing")]
public bool useShootEffects = true;
[Tooltip("Particle effect at shot (pooled)")]
public GameObject muzzleFlashPrefab; public GameObject muzzleFlashPrefab;
[Tooltip("Shoot sound")] [Tooltip("Shoot sound (played on AudioSource)")]
public AudioClip shootSound; public AudioClip shootSound;
[Header("Debug")] [Header("Debug")]
@@ -63,20 +59,14 @@ namespace DemonBoss.Magic
[Tooltip("Show gizmos in Scene View")] [Tooltip("Show gizmos in Scene View")]
public bool showGizmos = true; public bool showGizmos = true;
// Private variables
private Transform target; private Transform target;
private AudioSource audioSource; private AudioSource audioSource;
private Coroutine shootingCoroutine; private Coroutine shootingCoroutine;
private bool isActive = false; private bool isActive = false;
private int shotsFired = 0; private int shotsFired = 0;
private float lastShotTime = 0f; private float lastShotTime = 0f;
private Transform crystalTransform; private Transform crystalTransform;
/// <summary>
/// Component initialization
/// </summary>
private void Awake() private void Awake()
{ {
crystalTransform = transform; crystalTransform = transform;
@@ -91,6 +81,7 @@ namespace DemonBoss.Magic
if (muzzle == null) if (muzzle == null)
{ {
// Try to find a child named "muzzle"; fallback to self
Transform muzzleChild = crystalTransform.Find("muzzle"); Transform muzzleChild = crystalTransform.Find("muzzle");
if (muzzleChild != null) if (muzzleChild != null)
{ {
@@ -105,33 +96,27 @@ namespace DemonBoss.Magic
} }
} }
/// <summary>
/// Start - begin crystal operation
/// </summary>
private void Start() private void Start()
{ {
if (enableDebug) Debug.Log("[CrystalShooterAI] Crystal activated"); if (enableDebug) Debug.Log("[CrystalShooterAI] Crystal activated");
if (autoFindPlayer && target == null) if (autoFindPlayer && target == null)
{
FindPlayer(); FindPlayer();
}
StartShooting(); StartShooting();
} }
/// <summary> /// <summary>
/// Update - rotate crystal towards target /// Update tick: rotate towards target or idle spin.
/// </summary> /// </summary>
private void Update() private void Update()
{ {
if (!isActive) return; if (!isActive) return;
RotateTowardsTarget(); RotateTowardsTarget();
} }
/// <summary> /// <summary>
/// Find player automatically /// Attempts to find the player by tag (for turret-only aiming).
/// </summary> /// </summary>
private void FindPlayer() private void FindPlayer()
{ {
@@ -141,42 +126,37 @@ namespace DemonBoss.Magic
SetTarget(player.transform); SetTarget(player.transform);
if (enableDebug) Debug.Log("[CrystalShooterAI] Automatically found player"); if (enableDebug) Debug.Log("[CrystalShooterAI] Automatically found player");
} }
else else if (enableDebug)
{ {
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Cannot find player with tag: " + playerTag); Debug.LogWarning("[CrystalShooterAI] Cannot find player with tag: " + playerTag);
} }
} }
/// <summary> /// <summary>
/// Set target for crystal /// Sets the turret's aiming target (does NOT propagate to projectiles).
/// </summary> /// </summary>
/// <param name="newTarget">Target transform</param>
public void SetTarget(Transform newTarget) public void SetTarget(Transform newTarget)
{ {
target = newTarget; target = newTarget;
if (enableDebug && target != null) if (enableDebug && target != null)
{
Debug.Log($"[CrystalShooterAI] Set target: {target.name}"); Debug.Log($"[CrystalShooterAI] Set target: {target.name}");
}
} }
/// <summary> /// <summary>
/// Start shooting cycle /// Starts the timed shooting routine (fires until maxShots, then despawns).
/// </summary> /// </summary>
public void StartShooting() public void StartShooting()
{ {
if (isActive) return; if (isActive) return;
isActive = true; isActive = true;
shotsFired = 0; shotsFired = 0;
shootingCoroutine = StartCoroutine(ShootingCoroutine()); shootingCoroutine = StartCoroutine(ShootingCoroutine());
if (enableDebug) Debug.Log("[CrystalShooterAI] Starting shooting"); if (enableDebug) Debug.Log("[CrystalShooterAI] Starting shooting");
} }
/// <summary> /// <summary>
/// Stop shooting /// Stops the shooting routine immediately.
/// </summary> /// </summary>
public void StopShooting() public void StopShooting()
{ {
@@ -192,7 +172,8 @@ namespace DemonBoss.Magic
} }
/// <summary> /// <summary>
/// Main coroutine handling shooting cycle /// Main shooting loop: checks aim/range → spawns fireball → waits fireRate.
/// After finishing, waits a short delay and despawns the turret.
/// </summary> /// </summary>
private IEnumerator ShootingCoroutine() private IEnumerator ShootingCoroutine()
{ {
@@ -211,12 +192,11 @@ namespace DemonBoss.Magic
if (enableDebug) Debug.Log($"[CrystalShooterAI] Finished shooting ({shotsFired} shots)"); if (enableDebug) Debug.Log($"[CrystalShooterAI] Finished shooting ({shotsFired} shots)");
yield return new WaitForSeconds(despawnDelay); yield return new WaitForSeconds(despawnDelay);
DespawnCrystal(); DespawnCrystal();
} }
/// <summary> /// <summary>
/// Checks if crystal can shoot /// Aiming/range gate for firing.
/// </summary> /// </summary>
private bool CanShoot() private bool CanShoot()
{ {
@@ -232,7 +212,7 @@ namespace DemonBoss.Magic
} }
/// <summary> /// <summary>
/// Fires fireball towards target /// Spawns a fireball oriented towards the turret's current aim direction.
/// </summary> /// </summary>
private void FireFireball() private void FireFireball()
{ {
@@ -242,39 +222,41 @@ namespace DemonBoss.Magic
return; return;
} }
Vector3 shootDirection = crystalTransform.forward; Vector3 shootDirection;
if (target != null) if (target != null)
{ {
Vector3 targetCenter = target.position + Vector3.up * 1f; Vector3 targetCenter = target.position + Vector3.up * 1f;
shootDirection = (targetCenter - muzzle.position).normalized; shootDirection = (targetCenter - muzzle.position).normalized;
} }
else
GameObject fireball = LeanPool.Spawn(fireballPrefab, muzzle.position,
Quaternion.LookRotation(shootDirection));
Rigidbody fireballRb = fireball.GetComponent<Rigidbody>();
if (fireballRb != null)
{ {
fireballRb.linearVelocity = shootDirection * fireballSpeed; shootDirection = crystalTransform.forward;
} }
Vector3 spawnPosition = muzzle.position;
Quaternion spawnRotation = Quaternion.LookRotation(shootDirection);
LeanPool.Spawn(fireballPrefab, spawnPosition, spawnRotation);
PlayShootEffects(); PlayShootEffects();
if (enableDebug) if (enableDebug)
{ {
Debug.Log($"[CrystalShooterAI] Shot #{shotsFired + 1} in direction: {shootDirection}"); Debug.Log($"[CrystalShooterAI] Shot #{shotsFired + 1} at {spawnPosition} dir: {shootDirection}");
Debug.DrawRay(spawnPosition, shootDirection * 8f, Color.red, 2f);
} }
} }
/// <summary> /// <summary>
/// Plays shooting effects /// Plays muzzle VFX and shoot SFX (if enabled).
/// </summary> /// </summary>
private void PlayShootEffects() private void PlayShootEffects()
{ {
if (!useShootEffects) return;
if (muzzleFlashPrefab != null && muzzle != null) if (muzzleFlashPrefab != null && muzzle != null)
{ {
GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, muzzle.position, muzzle.rotation); GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, muzzle.position, muzzle.rotation);
LeanPool.Despawn(flash, 2f); LeanPool.Despawn(flash, 2f);
} }
@@ -285,14 +267,14 @@ namespace DemonBoss.Magic
} }
/// <summary> /// <summary>
/// Rotates crystal towards target or performs idle spin /// Smooth yaw rotation towards target; idles by spinning when no target.
/// </summary> /// </summary>
private void RotateTowardsTarget() private void RotateTowardsTarget()
{ {
if (target != null) if (target != null)
{ {
Vector3 directionToTarget = target.position - crystalTransform.position; Vector3 directionToTarget = target.position - crystalTransform.position;
directionToTarget.y = 0; directionToTarget.y = 0f;
if (directionToTarget != Vector3.zero) if (directionToTarget != Vector3.zero)
{ {
@@ -311,19 +293,17 @@ namespace DemonBoss.Magic
} }
/// <summary> /// <summary>
/// Despawns crystal from map /// Despawns the turret via Lean Pool.
/// </summary> /// </summary>
public void DespawnCrystal() public void DespawnCrystal()
{ {
if (enableDebug) Debug.Log("[CrystalShooterAI] Despawning crystal"); if (enableDebug) Debug.Log("[CrystalShooterAI] Despawning crystal");
StopShooting(); StopShooting();
LeanPool.Despawn(gameObject); LeanPool.Despawn(gameObject);
} }
/// <summary> /// <summary>
/// Forces immediate despawn (e.g. on boss death) /// Forces immediate despawn (e.g., boss death).
/// </summary> /// </summary>
public void ForceDespawn() public void ForceDespawn()
{ {
@@ -331,9 +311,7 @@ namespace DemonBoss.Magic
DespawnCrystal(); DespawnCrystal();
} }
/// <summary> /// <summary> Returns crystal state information. </summary>
/// Returns crystal state information
/// </summary>
public bool IsActive() => isActive; public bool IsActive() => isActive;
public int GetShotsFired() => shotsFired; public int GetShotsFired() => shotsFired;
@@ -343,7 +321,7 @@ namespace DemonBoss.Magic
public float GetTimeSinceLastShot() => Time.time - lastShotTime; public float GetTimeSinceLastShot() => Time.time - lastShotTime;
/// <summary> /// <summary>
/// Draws gizmos in Scene View /// Gizmos for range and aim visualization.
/// </summary> /// </summary>
private void OnDrawGizmosSelected() private void OnDrawGizmosSelected()
{ {
@@ -366,6 +344,7 @@ namespace DemonBoss.Magic
Gizmos.DrawLine(muzzle.position, muzzle.position + right); Gizmos.DrawLine(muzzle.position, muzzle.position + right);
Gizmos.DrawLine(muzzle.position, muzzle.position + left); Gizmos.DrawLine(muzzle.position, muzzle.position + left);
Gizmos.DrawLine(muzzle.position, muzzle.position + forward);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ GameObject:
- component: {fileID: 4577187839491108} - component: {fileID: 4577187839491108}
- component: {fileID: 198086061384069858} - component: {fileID: 198086061384069858}
- component: {fileID: 199127982807948490} - component: {fileID: 199127982807948490}
m_Layer: 0 m_Layer: 2
m_Name: FireEmbers (4) m_Name: FireEmbers (4)
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -4703,7 +4703,6 @@ ParticleSystemRenderer:
m_RendererPriority: 0 m_RendererPriority: 0
m_Materials: m_Materials:
- {fileID: 2100000, guid: 776677ab3818fb249adaca05f4e04545, type: 2} - {fileID: 2100000, guid: 776677ab3818fb249adaca05f4e04545, type: 2}
- {fileID: 0}
m_StaticBatchInfo: m_StaticBatchInfo:
firstSubMesh: 0 firstSubMesh: 0
subMeshCount: 0 subMeshCount: 0
@@ -4786,7 +4785,7 @@ Transform:
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068}
m_LocalPosition: {x: 6.67, y: 1, z: -53.73} m_LocalPosition: {x: 6.67, y: 1, z: -53.73}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 0.25, y: 0.25, z: 0.25}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 4577187839491108} - {fileID: 4577187839491108}
@@ -9552,7 +9551,6 @@ ParticleSystemRenderer:
m_RendererPriority: 0 m_RendererPriority: 0
m_Materials: m_Materials:
- {fileID: 2100000, guid: 3349fc02d104e2b458470ec533f51c16, type: 2} - {fileID: 2100000, guid: 3349fc02d104e2b458470ec533f51c16, type: 2}
- {fileID: 0}
m_StaticBatchInfo: m_StaticBatchInfo:
firstSubMesh: 0 firstSubMesh: 0
subMeshCount: 0 subMeshCount: 0
@@ -9620,14 +9618,14 @@ CapsuleCollider:
serializedVersion: 2 serializedVersion: 2
m_Bits: 0 m_Bits: 0
m_LayerOverridePriority: 0 m_LayerOverridePriority: 0
m_IsTrigger: 0 m_IsTrigger: 1
m_ProvidesContacts: 0 m_ProvidesContacts: 0
m_Enabled: 1 m_Enabled: 1
serializedVersion: 2 serializedVersion: 2
m_Radius: 0.5 m_Radius: 0.5
m_Height: 1 m_Height: 0.1
m_Direction: 1 m_Direction: 1
m_Center: {x: 0, y: 0, z: 0} m_Center: {x: 0.31801516, y: 0, z: 0}
--- !u!114 &6785567375430979834 --- !u!114 &6785567375430979834
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -9640,17 +9638,11 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 1a863cb5e6092ec4a936c4eb21bb9166, type: 3} m_Script: {fileID: 11500000, guid: 1a863cb5e6092ec4a936c4eb21bb9166, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
targetTag: Player
speed: 6
lockTime: 15
maxLifeTime: 30
arrivalTolerance: 0.25
damage: 25 damage: 25
damageOnce: 1
targetLayerMask:
serializedVersion: 2
m_Bits: 1410334711
impactEffectPrefab: {fileID: 132858, guid: c5cabec71c815b64ab6e2547b594ec81, type: 3}
impactSound: {fileID: 8300000, guid: 5dd60193d2ea89e47ae309eec2c3852e, type: 3}
knockbackForce: 5 knockbackForce: 5
maxLifetime: 5
destroyOnTerrain: 1
terrainLayerMask:
serializedVersion: 2
m_Bits: 1
enableDebug: 0 enableDebug: 0

View File

@@ -1,386 +0,0 @@
using Invector;
using Invector.vCharacterController;
using Lean.Pool;
using UnityEngine;
namespace DemonBoss.Magic
{
/// <summary>
/// Component handling damage and collisions for fireball fired by crystals
/// Deals damage on collision with player and automatically despawns
/// </summary>
[RequireComponent(typeof(Collider))]
public class FireballDamage : MonoBehaviour
{
[Header("Damage Configuration")]
[Tooltip("Damage dealt by fireball")]
public float damage = 25f;
[Tooltip("Whether fireball can deal damage only once")]
public bool damageOnce = true;
[Tooltip("Layer mask for targets that can be hit")]
public LayerMask targetLayerMask = -1;
[Header("Impact Effects")]
[Tooltip("Explosion effect prefab on hit")]
public GameObject impactEffectPrefab;
[Tooltip("Impact sound")]
public AudioClip impactSound;
[Tooltip("Knockback force on hit")]
public float knockbackForce = 5f;
[Header("Lifetime")]
[Tooltip("Maximum fireball lifetime in seconds (failsafe)")]
public float maxLifetime = 5f;
[Tooltip("Whether to destroy fireball on terrain collision")]
public bool destroyOnTerrain = true;
[Tooltip("Layer mask for terrain/obstacles")]
public LayerMask terrainLayerMask = 1;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
// Private variables
private bool hasDealtDamage = false;
private float spawnTime;
private AudioSource audioSource;
private Collider fireballCollider;
private Rigidbody fireballRigidbody;
/// <summary>
/// Component initialization
/// </summary>
private void Awake()
{
fireballCollider = GetComponent<Collider>();
fireballRigidbody = GetComponent<Rigidbody>();
if (fireballCollider != null && !fireballCollider.isTrigger)
{
fireballCollider.isTrigger = true;
if (enableDebug) Debug.Log("[FireballDamage] Set collider as trigger");
}
audioSource = GetComponent<AudioSource>();
if (audioSource == null && impactSound != null)
{
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.spatialBlend = 1f;
}
}
/// <summary>
/// Start - note spawn time and start failsafe timer
/// </summary>
private void Start()
{
spawnTime = Time.time;
hasDealtDamage = false;
Invoke(nameof(FailsafeDespawn), maxLifetime);
if (enableDebug) Debug.Log("[FireballDamage] Fireball initialized");
}
/// <summary>
/// Trigger collision handling - dealing damage
/// </summary>
/// <param name="other">Collider that fireball collides with</param>
private void OnTriggerEnter(Collider other)
{
if (enableDebug) Debug.Log($"[FireballDamage] Collision with: {other.name}");
if (IsValidTarget(other))
{
DealDamageToTarget(other);
CreateImpactEffect(other.transform.position);
DespawnFireball();
return;
}
if (destroyOnTerrain && IsTerrainCollision(other))
{
if (enableDebug) Debug.Log("[FireballDamage] Terrain collision");
CreateImpactEffect(transform.position);
DespawnFireball();
return;
}
}
/// <summary>
/// Checks if collider is valid target
/// </summary>
/// <param name="other">Collider to check</param>
/// <returns>True if it's valid target</returns>
private bool IsValidTarget(Collider other)
{
if ((targetLayerMask.value & (1 << other.gameObject.layer)) == 0)
return false;
if (damageOnce && hasDealtDamage)
return false;
var damageReceiver = other.GetComponent<vIDamageReceiver>();
var healthController = other.GetComponent<vHealthController>();
var character = other.GetComponent<vCharacter>();
var thirdPersonController = other.GetComponent<vThirdPersonController>();
if (damageReceiver == null) damageReceiver = other.GetComponentInParent<vIDamageReceiver>();
if (healthController == null) healthController = other.GetComponentInParent<vHealthController>();
if (character == null) character = other.GetComponentInParent<vCharacter>();
if (thirdPersonController == null) thirdPersonController = other.GetComponentInParent<vThirdPersonController>();
return damageReceiver != null || healthController != null || character != null || thirdPersonController != null;
}
/// <summary>
/// Checks if it's terrain collision
/// </summary>
/// <param name="other">Collider to check</param>
/// <returns>True if it's terrain</returns>
private bool IsTerrainCollision(Collider other)
{
return (terrainLayerMask.value & (1 << other.gameObject.layer)) != 0;
}
/// <summary>
/// Deals damage to target - ulepszona wersja
/// </summary>
/// <param name="targetCollider">Target collider</param>
private void DealDamageToTarget(Collider targetCollider)
{
if (enableDebug) Debug.Log($"[FireballDamage] Dealing {damage} damage to: {targetCollider.name}");
Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
Vector3 hitDirection = GetHitDirection(targetCollider);
vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage));
damageInfo.sender = transform;
damageInfo.hitPosition = hitPoint;
if (knockbackForce > 0f)
{
damageInfo.force = hitDirection * knockbackForce;
}
bool damageDealt = false;
var damageReceiver = targetCollider.GetComponent<vIDamageReceiver>();
if (damageReceiver == null) damageReceiver = targetCollider.GetComponentInParent<vIDamageReceiver>();
if (damageReceiver != null && !damageDealt)
{
damageReceiver.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vIDamageReceiver");
}
if (!damageDealt)
{
var healthController = targetCollider.GetComponent<vHealthController>();
if (healthController == null) healthController = targetCollider.GetComponentInParent<vHealthController>();
if (healthController != null)
{
healthController.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vHealthController");
}
}
if (!damageDealt)
{
var thirdPersonController = targetCollider.GetComponent<vThirdPersonController>();
if (thirdPersonController == null) thirdPersonController = targetCollider.GetComponentInParent<vThirdPersonController>();
if (thirdPersonController != null)
{
if (thirdPersonController is Beyond.bThirdPersonController beyondController)
{
if (!beyondController.GodMode && !beyondController.isImmortal)
{
thirdPersonController.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through bThirdPersonController");
}
else
{
if (enableDebug) Debug.Log("[FireballDamage] Player is immortal or in God Mode - no damage dealt");
}
}
else
{
thirdPersonController.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vThirdPersonController");
}
}
}
if (!damageDealt)
{
if (enableDebug) Debug.LogWarning("[FireballDamage] Could not deal damage - no valid damage receiver found!");
}
}
/// <summary>
/// Takes the closest point on the collider as the point of impact
/// </summary>
private Vector3 GetClosestPointOnCollider(Collider targetCollider)
{
return targetCollider.ClosestPoint(transform.position);
}
/// <summary>
/// Calculates the direction of impact for knockback
/// </summary>
private Vector3 GetHitDirection(Collider targetCollider)
{
Vector3 direction;
if (fireballRigidbody != null && fireballRigidbody.linearVelocity.magnitude > 0.1f)
{
direction = fireballRigidbody.linearVelocity.normalized;
}
else
{
direction = (targetCollider.transform.position - transform.position).normalized;
}
direction.y = Mathf.Max(0.2f, direction.y);
return direction.normalized;
}
/// <summary>
/// Creates impact effect
/// </summary>
/// <param name="impactPosition">Impact position</param>
private void CreateImpactEffect(Vector3 impactPosition)
{
if (impactEffectPrefab != null)
{
GameObject impact = LeanPool.Spawn(impactEffectPrefab, impactPosition, Quaternion.identity);
LeanPool.Despawn(impact, 3f);
if (enableDebug) Debug.Log("[FireballDamage] Impact effect created");
}
if (audioSource != null && impactSound != null)
{
audioSource.PlayOneShot(impactSound);
}
}
/// <summary>
/// Despawns fireball from map
/// </summary>
private void DespawnFireball()
{
if (enableDebug) Debug.Log("[FireballDamage] Despawning fireball");
CancelInvoke(nameof(FailsafeDespawn));
LeanPool.Despawn(gameObject);
}
/// <summary>
/// Failsafe despawn - removes fireball after maximum lifetime
/// </summary>
private void FailsafeDespawn()
{
if (enableDebug) Debug.Log("[FireballDamage] Failsafe despawn - lifetime exceeded");
DespawnFireball();
}
/// <summary>
/// Forces immediate despawn
/// </summary>
public void ForceDespawn()
{
if (enableDebug) Debug.Log("[FireballDamage] Forced despawn");
DespawnFireball();
}
/// <summary>
/// Checks if fireball has already dealt damage
/// </summary>
/// <returns>True if damage was already dealt</returns>
public bool HasDealtDamage()
{
return hasDealtDamage;
}
/// <summary>
/// Returns fireball lifetime
/// </summary>
/// <returns>Time in seconds since spawn</returns>
public float GetLifetime()
{
return Time.time - spawnTime;
}
/// <summary>
/// Sets new damage (e.g. for different difficulty levels)
/// </summary>
/// <param name="newDamage">New damage</param>
public void SetDamage(float newDamage)
{
damage = newDamage;
if (enableDebug) Debug.Log($"[FireballDamage] Set new damage: {damage}");
}
/// <summary>
/// Sets new knockback
/// </summary>
/// <param name="newKnockback">New knockback force</param>
public void SetKnockback(float newKnockback)
{
knockbackForce = newKnockback;
if (enableDebug) Debug.Log($"[FireballDamage] Set new knockback: {knockbackForce}");
}
/// <summary>
/// Resets fireball state (useful for pooling)
/// </summary>
public void ResetFireball()
{
hasDealtDamage = false;
spawnTime = Time.time;
CancelInvoke();
Invoke(nameof(FailsafeDespawn), maxLifetime);
if (enableDebug) Debug.Log("[FireballDamage] Fireball reset");
}
/// <summary>
/// Called on spawn by Lean Pool
/// </summary>
private void OnSpawn()
{
ResetFireball();
}
/// <summary>
/// Called on despawn by Lean Pool
/// </summary>
private void OnDespawn()
{
CancelInvoke();
}
}
}

View File

@@ -0,0 +1,314 @@
using Invector;
using Invector.vCharacterController;
using Lean.Pool;
using UnityEngine;
namespace DemonBoss.Magic
{
public class FireballProjectile : MonoBehaviour
{
#region Configuration
[Header("Targeting")]
[Tooltip("Tag of the target to chase while tracking phase is active.")]
public string targetTag = "Player";
[Tooltip("Vertical offset added to the target position (e.g., aim at chest/head instead of feet).")]
public float targetHeightOffset = 1.0f;
[Header("Movement")]
[Tooltip("Units per second.")]
public float speed = 6f;
[Tooltip("Seconds to track the player before locking to last known position.")]
public float lockTime = 15f;
[Tooltip("Seconds before the projectile auto-despawns regardless of state.")]
public float maxLifeTime = 30f;
[Tooltip("Distance threshold to consider we arrived at the locked position.")]
public float arrivalTolerance = 0.25f;
[Header("Damage")]
[Tooltip("Base damage dealt to the target.")]
public int damage = 20;
[Tooltip("Optional knockback impulse magnitude applied along hit direction.")]
public float knockbackForce = 0f;
[Header("Debug")]
[Tooltip("Enable verbose debug logs.")]
public bool enableDebug = false;
#endregion Configuration
#region Runtime State
private Transform player;
private Vector3 lockedTargetPos;
private bool isLocked = false;
private bool hasDealtDamage = false;
private float timer = 0f;
#endregion Runtime State
#region Unity Lifecycle
/// <summary>
/// Called when the object becomes enabled (including when spawned from a pool).
/// Resets runtime state and acquires the target by tag.
/// </summary>
private void OnEnable()
{
ResetRuntimeState();
AcquireTargetByTag();
}
/// <summary>
/// Per-frame update: handle tracking/lock movement and lifetime/despawn conditions.
/// </summary>
private void Update()
{
timer += Time.deltaTime;
// Transition to 'locked to last position' after lockTime
if (!isLocked && timer >= lockTime && player != null)
{
lockedTargetPos = player.position + Vector3.up * targetHeightOffset;
isLocked = true;
if (enableDebug) Debug.Log($"[Fireball] Locked to last known player position: {lockedTargetPos}");
}
// Determine current aim point
Vector3 aimPoint;
if (!isLocked && player != null)
{
// Tracking phase: follow live player position
aimPoint = player.position + Vector3.up * targetHeightOffset;
}
else if (isLocked)
{
// Locked phase: fly to memorized position
aimPoint = lockedTargetPos;
}
else
{
// Fallback: no player found, keep moving forward
aimPoint = transform.position + transform.forward * 1000f;
}
// Move towards aim point
Vector3 toTarget = aimPoint - transform.position;
Vector3 step = toTarget.normalized * speed * Time.deltaTime;
transform.position += step;
// Face movement direction (optional but nice for VFX)
if (step.sqrMagnitude > 0.0001f)
transform.forward = step.normalized;
// If locked and close enough to the locked point → despawn
if (isLocked && toTarget.magnitude <= arrivalTolerance)
{
if (enableDebug) Debug.Log("[Fireball] Arrived at locked position → Despawn");
Despawn();
return;
}
// Hard cap lifetime
if (timer >= maxLifeTime)
{
if (enableDebug) Debug.Log("[Fireball] Max lifetime reached → Despawn");
Despawn();
}
}
/// <summary>
/// Trigger hit handler applies damage when colliding with the intended target tag and then despawns.
/// </summary>
private void OnTriggerEnter(Collider other)
{
if (hasDealtDamage) return; // guard against multiple triggers in a single frame
if (other.CompareTag(targetTag))
{
DealDamageToTarget(other);
Despawn();
}
}
#endregion Unity Lifecycle
#region Target Acquisition
/// <summary>
/// Finds the target by tag (e.g., "Player"). Called on enable and can be retried if needed.
/// </summary>
private void AcquireTargetByTag()
{
var playerObj = GameObject.FindGameObjectWithTag(targetTag);
player = playerObj ? playerObj.transform : null;
if (enableDebug)
{
if (player != null) Debug.Log($"[Fireball] Target found by tag '{targetTag}'.");
else Debug.LogWarning($"[Fireball] No target with tag '{targetTag}' found moving forward fallback.");
}
}
#endregion Target Acquisition
#region Damage Pipeline (Invector-style)
/// <summary>
/// Applies damage using available receivers on the hit collider or its parents:
/// vIDamageReceiver → vHealthController → vThirdPersonController (with Beyond variant checks).
/// </summary>
private void DealDamageToTarget(Collider targetCollider)
{
if (enableDebug) Debug.Log($"[FireballDamage] Dealing {damage} damage to: {targetCollider.name}");
Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
Vector3 hitDirection = GetHitDirection(targetCollider);
// Build vDamage payload (Invector)
vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage))
{
sender = transform,
hitPosition = hitPoint
};
if (knockbackForce > 0f)
damageInfo.force = hitDirection * knockbackForce;
bool damageDealt = false;
// 1) Try generic vIDamageReceiver (collider or parent)
var damageReceiver = targetCollider.GetComponent<vIDamageReceiver>() ??
targetCollider.GetComponentInParent<vIDamageReceiver>();
if (damageReceiver != null && !damageDealt)
{
damageReceiver.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vIDamageReceiver");
}
// 2) Fallback to vHealthController
if (!damageDealt)
{
var healthController = targetCollider.GetComponent<vHealthController>() ??
targetCollider.GetComponentInParent<vHealthController>();
if (healthController != null)
{
healthController.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vHealthController");
}
}
// 3) Fallback to vThirdPersonController (handle Beyond variant)
if (!damageDealt)
{
var tpc = targetCollider.GetComponent<vThirdPersonController>() ??
targetCollider.GetComponentInParent<vThirdPersonController>();
if (tpc != null)
{
if (tpc is Beyond.bThirdPersonController beyond)
{
if (!beyond.GodMode && !beyond.isImmortal)
{
tpc.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through bThirdPersonController");
}
else
{
if (enableDebug) Debug.Log("[FireballDamage] Target is immortal / GodMode no damage dealt");
}
}
else
{
tpc.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vThirdPersonController");
}
}
}
if (!damageDealt && enableDebug)
Debug.LogWarning("[FireballDamage] Could not deal damage no valid damage receiver found!");
}
/// <summary>
/// Computes the closest point on the target collider to this projectile position.
/// </summary>
private Vector3 GetClosestPointOnCollider(Collider col)
{
return col.ClosestPoint(transform.position);
}
/// <summary>
/// Computes a reasonable hit direction from projectile to target center (fallbacks to forward).
/// </summary>
private Vector3 GetHitDirection(Collider col)
{
Vector3 dir = (col.bounds.center - transform.position).normalized;
return dir.sqrMagnitude > 0.0001f ? dir : transform.forward;
}
#endregion Damage Pipeline (Invector-style)
#region Pooling & Utilities
/// <summary>
/// Returns this projectile to the Lean Pool (safe to call on non-pooled too).
/// </summary>
private void Despawn()
{
if (enableDebug) Debug.Log("[Fireball] Despawn via LeanPool");
LeanPool.Despawn(gameObject);
}
/// <summary>
/// Clears/initializes runtime variables so pooled instances behave like fresh spawns.
/// </summary>
private void ResetRuntimeState()
{
timer = 0f;
isLocked = false;
hasDealtDamage = false;
lockedTargetPos = Vector3.zero;
}
#endregion Pooling & Utilities
#if UNITY_EDITOR
/// <summary>
/// Optional gizmo: shows lock radius and direction for quick debugging.
/// </summary>
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(transform.position, 0.25f);
// Draw a small forward arrow
Gizmos.DrawLine(transform.position, transform.position + transform.forward * 1.0f);
// Arrival tolerance sphere (only meaningful when locked)
if (isLocked)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(lockedTargetPos, arrivalTolerance);
}
}
#endif
}
}

View File

@@ -1,185 +1,187 @@
using Invector.vCharacterController.AI.FSMBehaviour; using Invector.vCharacterController.AI.FSMBehaviour;
using Lean.Pool; using Lean.Pool;
using UnityEngine; using UnityEngine;
namespace DemonBoss.Magic namespace DemonBoss.Magic
{ {
/// <summary> /// <summary>
/// StateAction for Magic Shield spell - boss casts magical shield for 5 seconds /// StateAction for Magic Shield spell - boss casts magical shield for 5 seconds
/// During casting boss stands still, after completion returns to Combat /// During casting boss stands still, after completion returns to Combat
/// </summary> /// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Cast Shield")] [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Cast Shield")]
public class SA_CastShield : vStateAction public class SA_CastShield : vStateAction
{ {
public override string categoryName => "DemonBoss/Magic"; public override string categoryName => "DemonBoss/Magic";
public override string defaultName => "Cast Shield"; public override string defaultName => "Cast Shield";
[Header("Shield Configuration")] [Header("Shield Configuration")]
[Tooltip("Prefab with magical shield particle effect")] [Tooltip("Prefab with magical shield particle effect")]
public GameObject shieldFXPrefab; public GameObject shieldFXPrefab;
[Tooltip("Transform where shield should appear (usually boss center)")] [Tooltip("Shield duration in seconds")]
public Transform shieldSpawnPoint; public float shieldDuration = 5f;
[Tooltip("Shield duration in seconds")] [Tooltip("Animator bool parameter name for blocking state")]
public float shieldDuration = 5f; public string animatorBlockingBool = "IsBlocking";
[Tooltip("Animator trigger name for shield casting animation")] [Header("Debug")]
public string animatorTrigger = "CastShield"; [Tooltip("Enable debug logging")]
public bool enableDebug = false;
[Header("Debug")]
[Tooltip("Enable debug logging")] private GameObject spawnedShield;
public bool enableDebug = false; private Animator npcAnimator;
private Transform npcTransform;
private GameObject spawnedShield; private float shieldStartTime;
private bool shieldActive = false;
private float shieldStartTime;
private bool shieldActive = false; /// <summary>
/// Main action execution method called by FSM
/// <summary> /// </summary>
/// Main action execution method called by FSM public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
/// </summary> {
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate) if (executionType == vFSMComponentExecutionType.OnStateEnter)
{ {
if (executionType == vFSMComponentExecutionType.OnStateEnter) OnStateEnter(fsmBehaviour);
{ }
OnStateEnter(fsmBehaviour); else if (executionType == vFSMComponentExecutionType.OnStateUpdate)
} {
else if (executionType == vFSMComponentExecutionType.OnStateUpdate) OnStateUpdate(fsmBehaviour);
{ }
OnStateUpdate(fsmBehaviour); else if (executionType == vFSMComponentExecutionType.OnStateExit)
} {
else if (executionType == vFSMComponentExecutionType.OnStateExit) OnStateExit(fsmBehaviour);
{ }
OnStateExit(fsmBehaviour); }
}
} /// <summary>
/// Called when entering state - starts shield casting
/// <summary> /// </summary>
/// Called when entering state - starts shield casting private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
/// </summary> {
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour) if (enableDebug) Debug.Log("[SA_CastShield] Entering shield casting state");
{
if (enableDebug) Debug.Log("[SA_CastShield] Entering shield casting state"); // Store NPC references
npcTransform = fsmBehaviour.transform;
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; npcAnimator = npcTransform.GetComponent<Animator>();
if (aiController != null)
{ var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
aiController.Stop(); if (aiController != null)
if (enableDebug) Debug.Log("[SA_CastShield] AI stopped"); {
} aiController.Stop();
if (enableDebug) Debug.Log("[SA_CastShield] AI stopped");
var animator = fsmBehaviour.transform.GetComponent<Animator>(); }
if (animator != null && !string.IsNullOrEmpty(animatorTrigger))
{ if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
animator.SetTrigger(animatorTrigger); {
if (enableDebug) Debug.Log($"[SA_CastShield] Set trigger: {animatorTrigger}"); npcAnimator.SetBool(animatorBlockingBool, true);
} if (enableDebug) Debug.Log($"[SA_CastShield] Set bool: {animatorBlockingBool} = true");
}
SpawnShieldEffect(fsmBehaviour);
SpawnShieldEffect(fsmBehaviour);
shieldStartTime = Time.time;
shieldActive = true; shieldStartTime = Time.time;
} shieldActive = true;
}
/// <summary>
/// Called every frame during state duration /// <summary>
/// </summary> /// Called every frame during state duration
private void OnStateUpdate(vIFSMBehaviourController fsmBehaviour) /// </summary>
{ private void OnStateUpdate(vIFSMBehaviourController fsmBehaviour)
if (shieldActive && Time.time - shieldStartTime >= shieldDuration) {
{ if (shieldActive && Time.time - shieldStartTime >= shieldDuration)
if (enableDebug) Debug.Log("[SA_CastShield] Shield time passed, finishing state"); {
FinishShield(fsmBehaviour); if (enableDebug) Debug.Log("[SA_CastShield] Shield time passed, finishing state");
} FinishShield(fsmBehaviour);
} }
}
/// <summary>
/// Called when exiting state - cleanup /// <summary>
/// </summary> /// Called when exiting state - cleanup
private void OnStateExit(vIFSMBehaviourController fsmBehaviour) /// </summary>
{ private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
if (enableDebug) Debug.Log("[SA_CastShield] Exiting shield state"); {
if (enableDebug) Debug.Log("[SA_CastShield] Exiting shield state");
if (shieldActive)
{ if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
CleanupShield(); {
} npcAnimator.SetBool(animatorBlockingBool, false);
if (enableDebug) Debug.Log($"[SA_CastShield] Set bool: {animatorBlockingBool} = false");
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; }
if (aiController != null)
{ if (shieldActive)
if (enableDebug) Debug.Log("[SA_CastShield] AI resumed"); {
} CleanupShield();
} }
/// <summary> var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
/// Spawns magical shield particle effect if (aiController != null)
/// </summary> {
private void SpawnShieldEffect(vIFSMBehaviourController fsmBehaviour) if (enableDebug) Debug.Log("[SA_CastShield] AI resumed");
{ }
if (shieldFXPrefab == null) }
{
Debug.LogWarning("[SA_CastShield] Missing shieldFXPrefab!"); /// <summary>
return; /// Spawns magical shield particle effect
} /// </summary>
private void SpawnShieldEffect(vIFSMBehaviourController fsmBehaviour)
Vector3 spawnPosition = shieldSpawnPoint != null ? {
shieldSpawnPoint.position : fsmBehaviour.transform.position; if (shieldFXPrefab == null)
{
spawnedShield = LeanPool.Spawn(shieldFXPrefab, spawnPosition, Debug.LogWarning("[SA_CastShield] Missing shieldFXPrefab!");
shieldSpawnPoint != null ? shieldSpawnPoint.rotation : fsmBehaviour.transform.rotation); return;
}
if (enableDebug) Debug.Log($"[SA_CastShield] Shield spawned at position: {spawnPosition}");
// Spawn shield at NPC's position and rotation
if (spawnedShield != null && shieldSpawnPoint != null) Vector3 spawnPosition = npcTransform.position;
{ Quaternion spawnRotation = npcTransform.rotation;
spawnedShield.transform.SetParent(shieldSpawnPoint);
} spawnedShield = LeanPool.Spawn(shieldFXPrefab, spawnPosition, spawnRotation);
}
if (enableDebug) Debug.Log($"[SA_CastShield] Shield spawned at NPC position: {spawnPosition}");
/// <summary> }
/// Finishes shield operation and transitions to next state
/// </summary> /// <summary>
private void FinishShield(vIFSMBehaviourController fsmBehaviour) /// Finishes shield operation and transitions to next state
{ /// </summary>
shieldActive = false; private void FinishShield(vIFSMBehaviourController fsmBehaviour)
CleanupShield(); {
shieldActive = false;
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Shield", 15f); CleanupShield();
// End state - FSM will transition to next state DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Shield", 15f);
// FYI: In Invector FSM, state completion is handled automatically
} // End state - FSM will transition to next state
// FYI: In Invector FSM, state completion is handled automatically
/// <summary> }
/// Cleans up spawned shield
/// </summary> /// <summary>
private void CleanupShield() /// Cleans up spawned shield
{ /// </summary>
if (spawnedShield != null) private void CleanupShield()
{ {
LeanPool.Despawn(spawnedShield); if (spawnedShield != null)
spawnedShield = null; {
if (enableDebug) Debug.Log("[SA_CastShield] Shield despawned"); LeanPool.Despawn(spawnedShield);
} spawnedShield = null;
} if (enableDebug) Debug.Log("[SA_CastShield] Shield despawned");
}
/// <summary> }
/// Checks if shield is currently active
/// </summary> /// <summary>
public bool IsShieldActive() /// Checks if shield is currently active
{ /// </summary>
return shieldActive; public bool IsShieldActive()
} {
return shieldActive;
/// <summary> }
/// Returns remaining shield time
/// </summary> /// <summary>
public float GetRemainingShieldTime() /// Returns remaining shield time
{ /// </summary>
if (!shieldActive) return 0f; public float GetRemainingShieldTime()
return Mathf.Max(0f, shieldDuration - (Time.time - shieldStartTime)); {
} if (!shieldActive) return 0f;
} return Mathf.Max(0f, shieldDuration - (Time.time - shieldStartTime));
}
}
} }

View File

@@ -1,298 +1,322 @@
using Invector.vCharacterController.AI.FSMBehaviour; using Invector.vCharacterController.AI.FSMBehaviour;
using Lean.Pool; using Lean.Pool;
using UnityEngine; using UnityEngine;
namespace DemonBoss.Magic namespace DemonBoss.Magic
{ {
/// <summary> /// <summary>
/// StateAction for intelligent crystal turret spawning /// StateAction for intelligent crystal turret spawning
/// Searches for optimal position in 2-6m ring from boss, prefers "behind boss" relative to player /// Searches for optimal position in 2-6m ring from boss, prefers "behind boss" relative to player
/// </summary> /// </summary>
[CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn Turret Smart")] [CreateAssetMenu(menuName = "Invector/FSM/Actions/DemonBoss/Spawn Turret Smart")]
public class SA_SpawnTurretSmart : vStateAction public class SA_SpawnTurretSmart : vStateAction
{ {
public override string categoryName => "DemonBoss/Magic"; public override string categoryName => "DemonBoss/Magic";
public override string defaultName => "Spawn Turret Smart"; public override string defaultName => "Spawn Turret Smart";
[Header("Turret Configuration")] [Header("Turret Configuration")]
[Tooltip("Crystal prefab with CrystalShooterAI component")] [Tooltip("Crystal prefab with CrystalShooterAI component")]
public GameObject crystalPrefab; public GameObject crystalPrefab;
[Tooltip("Minimum distance from boss for crystal spawn")] [Tooltip("Minimum distance from boss for crystal spawn")]
public float minSpawnDistance = 2f; public float minSpawnDistance = 2f;
[Tooltip("Maximum distance from boss for crystal spawn")] [Tooltip("Maximum distance from boss for crystal spawn")]
public float maxSpawnDistance = 6f; public float maxSpawnDistance = 6f;
[Tooltip("Collision check radius when choosing position")] [Tooltip("Collision check radius when choosing position")]
public float obstacleCheckRadius = 1f; public float obstacleCheckRadius = 1f;
[Tooltip("Height above ground for raycast ground checking")] [Tooltip("Height above ground for raycast ground checking")]
public float groundCheckHeight = 2f; public float groundCheckHeight = 2f;
[Tooltip("Layer mask for obstacles")] [Tooltip("Layer mask for obstacles")]
public LayerMask obstacleLayerMask = -1; public LayerMask obstacleLayerMask = -1;
[Tooltip("Layer mask for ground")] [Tooltip("Layer mask for ground")]
public LayerMask groundLayerMask = -1; public LayerMask groundLayerMask = -1;
[Tooltip("Animator trigger name for crystal casting animation")] [Tooltip("Animator bool parameter name for blocking state")]
public string animatorTrigger = "CastCrystal"; public string animatorBlockingBool = "IsBlocking";
[Header("Smart Positioning")] [Header("Smart Positioning")]
[Tooltip("Preference multiplier for positions behind boss (relative to player)")] [Tooltip("Preference multiplier for positions behind boss (relative to player)")]
public float backPreferenceMultiplier = 2f; public float backPreferenceMultiplier = 2f;
[Tooltip("Number of attempts to find valid position")] [Tooltip("Number of attempts to find valid position")]
public int maxSpawnAttempts = 12; public int maxSpawnAttempts = 12;
[Header("Debug")] [Header("Debug")]
[Tooltip("Enable debug logging")] [Tooltip("Enable debug logging")]
public bool enableDebug = false; public bool enableDebug = false;
[Tooltip("Show gizmos in Scene View")] [Tooltip("Show gizmos in Scene View")]
public bool showGizmos = true; public bool showGizmos = true;
private GameObject spawnedCrystal; private GameObject spawnedCrystal;
private Animator npcAnimator;
private Transform playerTransform; private Transform npcTransform;
private Transform playerTransform;
/// <summary>
/// Main action execution method called by FSM /// <summary>
/// </summary> /// Main action execution method called by FSM
public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate) /// </summary>
{ public override void DoAction(vIFSMBehaviourController fsmBehaviour, vFSMComponentExecutionType executionType = vFSMComponentExecutionType.OnStateUpdate)
if (executionType == vFSMComponentExecutionType.OnStateEnter) {
{ if (executionType == vFSMComponentExecutionType.OnStateEnter)
OnStateEnter(fsmBehaviour); {
} OnStateEnter(fsmBehaviour);
} }
else if (executionType == vFSMComponentExecutionType.OnStateExit)
/// <summary> {
/// Called when entering state - intelligently spawns crystal OnStateExit(fsmBehaviour);
/// </summary> }
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour) }
{
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Starting intelligent crystal spawn"); /// <summary>
/// Called when entering state - intelligently spawns crystal
FindPlayer(fsmBehaviour); /// </summary>
private void OnStateEnter(vIFSMBehaviourController fsmBehaviour)
var animator = fsmBehaviour.transform.GetComponent<Animator>(); {
if (animator != null && !string.IsNullOrEmpty(animatorTrigger)) if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Starting intelligent crystal spawn");
{
animator.SetTrigger(animatorTrigger); // Store NPC references
if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set trigger: {animatorTrigger}"); npcTransform = fsmBehaviour.transform;
} npcAnimator = npcTransform.GetComponent<Animator>();
SpawnCrystalSmart(fsmBehaviour); FindPlayer(fsmBehaviour);
DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f); if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
} {
npcAnimator.SetBool(animatorBlockingBool, true);
/// <summary> if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set bool: {animatorBlockingBool} = true");
/// Finds player transform }
/// </summary>
private void FindPlayer(vIFSMBehaviourController fsmBehaviour) SpawnCrystalSmart(fsmBehaviour);
{
GameObject player = GameObject.FindGameObjectWithTag("Player"); DEC_CheckCooldown.SetCooldownStatic(fsmBehaviour, "Turret", 12f);
if (player != null) }
{
playerTransform = player.transform; /// <summary>
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found by tag"); /// Called when exiting state - cleanup
return; /// </summary>
} private void OnStateExit(vIFSMBehaviourController fsmBehaviour)
{
var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI; if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Exiting turret spawn state");
if (aiController != null && aiController.currentTarget != null)
{ if (npcAnimator != null && !string.IsNullOrEmpty(animatorBlockingBool))
playerTransform = aiController.currentTarget.transform; {
if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found through AI target"); npcAnimator.SetBool(animatorBlockingBool, false);
return; if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Set bool: {animatorBlockingBool} = false");
} }
}
if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Player not found!");
} /// <summary>
/// Finds player transform
/// <summary> /// </summary>
/// Intelligently spawns crystal in optimal position private void FindPlayer(vIFSMBehaviourController fsmBehaviour)
/// </summary> {
private void SpawnCrystalSmart(vIFSMBehaviourController fsmBehaviour) GameObject player = GameObject.FindGameObjectWithTag("Player");
{ if (player != null)
if (crystalPrefab == null) {
{ playerTransform = player.transform;
Debug.LogError("[SA_SpawnTurretSmart] Missing crystalPrefab!"); if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found by tag");
return; return;
} }
Vector3 bestPosition = Vector3.zero; var aiController = fsmBehaviour as Invector.vCharacterController.AI.vIControlAI;
bool foundValidPosition = false; if (aiController != null && aiController.currentTarget != null)
float bestScore = float.MinValue; {
playerTransform = aiController.currentTarget.transform;
Vector3 bossPos = fsmBehaviour.transform.position; if (enableDebug) Debug.Log("[SA_SpawnTurretSmart] Player found through AI target");
Vector3 playerDirection = Vector3.zero; return;
}
if (playerTransform != null)
{ if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Player not found!");
playerDirection = (playerTransform.position - bossPos).normalized; }
}
/// <summary>
for (int i = 0; i < maxSpawnAttempts; i++) /// Intelligently spawns crystal in optimal position
{ /// </summary>
float angle = (360f / maxSpawnAttempts) * i + Random.Range(-15f, 15f); private void SpawnCrystalSmart(vIFSMBehaviourController fsmBehaviour)
Vector3 direction = new Vector3(Mathf.Cos(angle * Mathf.Deg2Rad), 0, Mathf.Sin(angle * Mathf.Deg2Rad)); {
if (crystalPrefab == null)
float distance = Random.Range(minSpawnDistance, maxSpawnDistance); {
Vector3 testPosition = bossPos + direction * distance; Debug.LogError("[SA_SpawnTurretSmart] Missing crystalPrefab!");
return;
if (IsPositionValid(testPosition, out Vector3 groundPosition)) }
{
float score = EvaluatePosition(groundPosition, playerDirection, direction); Vector3 bestPosition = Vector3.zero;
bool foundValidPosition = false;
if (score > bestScore) float bestScore = float.MinValue;
{
bestScore = score; Vector3 bossPos = npcTransform.position;
bestPosition = groundPosition; Vector3 playerDirection = Vector3.zero;
foundValidPosition = true;
} if (playerTransform != null)
} {
} playerDirection = (playerTransform.position - bossPos).normalized;
}
if (foundValidPosition)
{ for (int i = 0; i < maxSpawnAttempts; i++)
SpawnCrystal(bestPosition, fsmBehaviour); {
if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Crystal spawned at position: {bestPosition} (score: {bestScore:F2})"); float angle = (360f / maxSpawnAttempts) * i + Random.Range(-15f, 15f);
} Vector3 direction = new Vector3(Mathf.Cos(angle * Mathf.Deg2Rad), 0, Mathf.Sin(angle * Mathf.Deg2Rad));
else
{ float distance = Random.Range(minSpawnDistance, maxSpawnDistance);
Vector3 fallbackPos = bossPos + fsmBehaviour.transform.forward * minSpawnDistance; Vector3 testPosition = bossPos + direction * distance;
SpawnCrystal(fallbackPos, fsmBehaviour);
if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Using fallback position"); if (IsPositionValid(testPosition, out Vector3 groundPosition))
} {
} float score = EvaluatePosition(groundPosition, playerDirection, direction, bossPos);
/// <summary> if (score > bestScore)
/// Checks if position is valid (no obstacles, has ground) {
/// </summary> bestScore = score;
private bool IsPositionValid(Vector3 position, out Vector3 groundPosition) bestPosition = groundPosition;
{ foundValidPosition = true;
groundPosition = position; }
}
if (Physics.CheckSphere(position + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask)) }
{
return false; if (foundValidPosition)
} {
SpawnCrystal(bestPosition, fsmBehaviour);
Ray groundRay = new Ray(position + Vector3.up * groundCheckHeight, Vector3.down); if (enableDebug) Debug.Log($"[SA_SpawnTurretSmart] Crystal spawned at position: {bestPosition} (score: {bestScore:F2})");
if (Physics.Raycast(groundRay, out RaycastHit hit, groundCheckHeight + 2f, groundLayerMask)) }
{ else
groundPosition = hit.point; {
Vector3 fallbackPos = bossPos + npcTransform.forward * minSpawnDistance;
if (Physics.CheckSphere(groundPosition + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask)) SpawnCrystal(fallbackPos, fsmBehaviour);
{ if (enableDebug) Debug.LogWarning("[SA_SpawnTurretSmart] Using fallback position");
return false; }
} }
return true; /// <summary>
} /// Checks if position is valid (no obstacles, has ground)
/// </summary>
return false; private bool IsPositionValid(Vector3 position, out Vector3 groundPosition)
} {
groundPosition = position;
/// <summary>
/// Evaluates position quality (higher score = better position) if (Physics.CheckSphere(position + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
/// </summary> {
private float EvaluatePosition(Vector3 position, Vector3 playerDirection, Vector3 positionDirection) return false;
{ }
float score = 0f;
Ray groundRay = new Ray(position + Vector3.up * groundCheckHeight, Vector3.down);
if (playerTransform != null && playerDirection != Vector3.zero) if (Physics.Raycast(groundRay, out RaycastHit hit, groundCheckHeight + 2f, groundLayerMask))
{ {
float angleToPlayer = Vector3.Angle(-playerDirection, positionDirection); groundPosition = hit.point;
// The smaller the angle (closer to "behind"), the better the score if (Physics.CheckSphere(groundPosition + Vector3.up * obstacleCheckRadius, obstacleCheckRadius, obstacleLayerMask))
float backScore = (180f - angleToPlayer) / 180f; {
score += backScore * backPreferenceMultiplier; return false;
} }
Vector3 bossPos = new Vector3(); return true;
float distance = Vector3.Distance(position, bossPos); }
float optimalDistance = (minSpawnDistance + maxSpawnDistance) * 0.5f;
float distanceScore = 1f - Mathf.Abs(distance - optimalDistance) / maxSpawnDistance; return false;
score += distanceScore; }
score += Random.Range(-0.1f, 0.1f); /// <summary>
/// Evaluates position quality (higher score = better position)
return score; /// </summary>
} private float EvaluatePosition(Vector3 position, Vector3 playerDirection, Vector3 positionDirection, Vector3 bossPos)
{
/// <summary> float score = 0f;
/// Spawns crystal at given position
/// </summary> if (playerTransform != null && playerDirection != Vector3.zero)
private void SpawnCrystal(Vector3 position, vIFSMBehaviourController fsmBehaviour) {
{ float angleToPlayer = Vector3.Angle(-playerDirection, positionDirection);
Quaternion rotation = Quaternion.identity;
if (playerTransform != null) // The smaller the angle (closer to "behind"), the better the score
{ float backScore = (180f - angleToPlayer) / 180f;
Vector3 lookDirection = (playerTransform.position - position).normalized; score += backScore * backPreferenceMultiplier;
lookDirection.y = 0; }
if (lookDirection != Vector3.zero)
{ float distance = Vector3.Distance(position, bossPos);
rotation = Quaternion.LookRotation(lookDirection); float optimalDistance = (minSpawnDistance + maxSpawnDistance) * 0.5f;
} float distanceScore = 1f - Mathf.Abs(distance - optimalDistance) / maxSpawnDistance;
} score += distanceScore;
spawnedCrystal = LeanPool.Spawn(crystalPrefab, position, rotation); score += Random.Range(-0.1f, 0.1f);
var shooterAI = spawnedCrystal.GetComponent<CrystalShooterAI>(); return score;
if (shooterAI == null) }
{
Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!"); /// <summary>
} /// Spawns crystal at given position
else /// </summary>
{ private void SpawnCrystal(Vector3 position, vIFSMBehaviourController fsmBehaviour)
if (playerTransform != null) {
{ Quaternion rotation = Quaternion.identity;
shooterAI.SetTarget(playerTransform); if (playerTransform != null)
} {
} Vector3 lookDirection = (playerTransform.position - position).normalized;
} lookDirection.y = 0;
if (lookDirection != Vector3.zero)
/// <summary> {
/// Draws gizmos in Scene View for debugging rotation = Quaternion.LookRotation(lookDirection);
/// </summary> }
private void OnDrawGizmosSelected() }
{
if (!showGizmos) return; spawnedCrystal = LeanPool.Spawn(crystalPrefab, position, rotation);
Vector3 pos = new Vector3(); var shooterAI = spawnedCrystal.GetComponent<CrystalShooterAI>();
if (shooterAI == null)
// Spawn ring {
Gizmos.color = Color.green; Debug.LogError("[SA_SpawnTurretSmart] Crystal prefab doesn't have CrystalShooterAI component!");
DrawWireCircle(pos, minSpawnDistance); }
Gizmos.color = Color.red; else
DrawWireCircle(pos, maxSpawnDistance); {
if (playerTransform != null)
// Obstacle check radius {
Gizmos.color = Color.yellow; shooterAI.SetTarget(playerTransform);
Gizmos.DrawWireSphere(pos + Vector3.up * obstacleCheckRadius, obstacleCheckRadius); }
} }
}
/// <summary>
/// Helper method for drawing circles /// <summary>
/// </summary> /// Draws gizmos in Scene View for debugging
private void DrawWireCircle(Vector3 center, float radius) /// </summary>
{ private void OnDrawGizmosSelected()
int segments = 32; {
float angle = 0f; if (!showGizmos) return;
Vector3 prevPoint = center + new Vector3(radius, 0, 0);
if (npcTransform != null)
for (int i = 1; i <= segments; i++) {
{ Vector3 pos = npcTransform.position;
angle = (float)i / segments * 360f * Mathf.Deg2Rad;
Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius); // Spawn ring
Gizmos.DrawLine(prevPoint, newPoint); Gizmos.color = Color.green;
prevPoint = newPoint; DrawWireCircle(pos, minSpawnDistance);
} Gizmos.color = Color.red;
} DrawWireCircle(pos, maxSpawnDistance);
}
// Obstacle check radius
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(pos + Vector3.up * obstacleCheckRadius, obstacleCheckRadius);
}
}
/// <summary>
/// Helper method for drawing circles
/// </summary>
private void DrawWireCircle(Vector3 center, float radius)
{
int segments = 32;
float angle = 0f;
Vector3 prevPoint = center + new Vector3(radius, 0, 0);
for (int i = 1; i <= segments; i++)
{
angle = (float)i / segments * 360f * Mathf.Deg2Rad;
Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
Gizmos.DrawLine(prevPoint, newPoint);
prevPoint = newPoint;
}
}
}
} }

View File

@@ -13,7 +13,7 @@ GameObject:
- component: {fileID: 2323612} - component: {fileID: 2323612}
- component: {fileID: 13662188} - component: {fileID: 13662188}
- component: {fileID: 7020133711031364094} - component: {fileID: 7020133711031364094}
m_Layer: 0 m_Layer: 26
m_Name: Turet m_Name: Turet
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -32,7 +32,8 @@ Transform:
m_LocalPosition: {x: -3.8250632, y: -7.1673625e-17, z: 3.2278929} m_LocalPosition: {x: -3.8250632, y: -7.1673625e-17, z: 3.2278929}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children:
- {fileID: 8411050209285398990}
m_Father: {fileID: 0} m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &3365858 --- !u!33 &3365858
@@ -123,20 +124,51 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 906d915677721914dbe9708f218f574a, type: 3} m_Script: {fileID: 11500000, guid: 906d915677721914dbe9708f218f574a, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
muzzle: {fileID: 417168} muzzle: {fileID: 8411050209285398990}
fireballPrefab: {fileID: 1947871717301538, guid: 9591667a35466484096c6e63785e136c, fireballPrefab: {fileID: 1947871717301538, guid: 9591667a35466484096c6e63785e136c,
type: 3} type: 3}
fireballSpeed: 28 fireRate: 5
fireRate: 0.7 maxShots: 3
maxShots: 10 despawnDelay: 10
despawnDelay: 3
turnSpeed: 120 turnSpeed: 120
idleSpinSpeed: 30 idleSpinSpeed: 30
aimTolerance: 5 aimTolerance: 5
autoFindPlayer: 1 autoFindPlayer: 1
playerTag: Player playerTag: Player
maxShootingRange: 50 maxShootingRange: 50
useShootEffects: 0
muzzleFlashPrefab: {fileID: 0} muzzleFlashPrefab: {fileID: 0}
shootSound: {fileID: 8300000, guid: d44a96f293b66b74ea13a2e6fcc6c8fa, type: 3} shootSound: {fileID: 8300000, guid: d44a96f293b66b74ea13a2e6fcc6c8fa, type: 3}
enableDebug: 0 enableDebug: 0
showGizmos: 1 showGizmos: 1
--- !u!1 &6534933121460188189
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8411050209285398990}
m_Layer: 26
m_Name: Spawnpoint
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &8411050209285398990
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6534933121460188189}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 3.114, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 417168}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8a6b7d8e31ffda843b06eaa079b5ed5d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

8
Assets/AI/Gigant.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b9f57b9ef3a51cd4285f9c4514b79802
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 05db168aa73c6d7489b4930e8ac0f2ce
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
using RootMotion.FinalIK;
using UnityEngine;
public class LookAtPlayerInitialization : MonoBehaviour
{
public LookAtIK lookAtIK;
public string playerTag = "Player";
private Transform player;
private void OnEnable()
{
GameObject playerObj = GameObject.FindGameObjectWithTag(playerTag);
if (playerObj != null)
{
player = playerObj.transform;
if (lookAtIK != null)
{
lookAtIK.solver.target = player;
lookAtIK.solver.IKPositionWeight = 1f;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 529e37587f83e4b4893a5814cd0cb915

View File

@@ -15,7 +15,7 @@ MonoBehaviour:
m_DefaultGroup: bc074d00982114f0388e9c98de91d44f m_DefaultGroup: bc074d00982114f0388e9c98de91d44f
m_currentHash: m_currentHash:
serializedVersion: 2 serializedVersion: 2
Hash: 1efe8cf9686e6b90ba179faa51fb53d2 Hash: ef672e941747c2eb6b19db828d740a6a
m_OptimizeCatalogSize: 0 m_OptimizeCatalogSize: 0
m_BuildRemoteCatalog: 0 m_BuildRemoteCatalog: 0
m_CatalogRequestsTimeout: 0 m_CatalogRequestsTimeout: 0

View File

@@ -124,7 +124,7 @@ Material:
- _CastShadows: 1 - _CastShadows: 1
- _ClearCoatMask: 0 - _ClearCoatMask: 0
- _ClearCoatSmoothness: 0 - _ClearCoatSmoothness: 0
- _Cull: 2 - _Cull: 0
- _Cutoff: 0.5 - _Cutoff: 0.5
- _DetailAlbedoMapScale: 1 - _DetailAlbedoMapScale: 1
- _DetailNormalMapScale: 1 - _DetailNormalMapScale: 1
@@ -156,7 +156,7 @@ Material:
- _WorkflowMode: 1 - _WorkflowMode: 1
- _ZTest: 4 - _ZTest: 4
- _ZWrite: 1 - _ZWrite: 1
- _ZWriteControl: 0 - _ZWriteControl: 1
m_Colors: m_Colors:
- BaseColor: {r: 0.7830189, g: 0.6195203, b: 0.5798772, a: 0} - BaseColor: {r: 0.7830189, g: 0.6195203, b: 0.5798772, a: 0}
- Color_613d1588816440ec9b17710effb7528b: {r: 0, g: 13.98681, b: 714.8679, a: 0} - Color_613d1588816440ec9b17710effb7528b: {r: 0, g: 13.98681, b: 714.8679, a: 0}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff