This commit is contained in:
SzymonMis
2026-01-20 21:04:22 +01:00
parent 09dfdfa066
commit 1beff44ada
12 changed files with 559 additions and 16 deletions

View File

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

View File

@@ -0,0 +1,101 @@
using UnityEngine;
using Invector;
public class SummonDamageReceiver : MonoBehaviour, vIHealthController
{
[Header("Health")]
[SerializeField] private int maxHealth = 40;
[SerializeField] private float currentHealth = 40f;
[Header("Death")]
[SerializeField] private bool destroyOnDeath = true;
[SerializeField] private float destroyDelay = 0.1f;
[SerializeField] private GameObject deathEffect;
[Header("Debug")]
[SerializeField] private bool enableDebug = false;
[SerializeField] private OnReceiveDamage _onStartReceiveDamage = new OnReceiveDamage();
[SerializeField] private OnReceiveDamage _onReceiveDamage = new OnReceiveDamage();
[SerializeField] private OnDead _onDead = new OnDead();
public OnReceiveDamage onStartReceiveDamage => _onStartReceiveDamage;
public OnReceiveDamage onReceiveDamage => _onReceiveDamage;
public OnDead onDead => _onDead;
public float currentHealth => this.currentHealth;
public int MaxHealth => maxHealth;
public bool isDead { get; set; }
private void Awake()
{
ResetHealth();
}
public void TakeDamage(vDamage damage)
{
if (isDead) return;
_onStartReceiveDamage.Invoke(damage);
int dmg = Mathf.RoundToInt(damage.damageValue);
if (dmg <= 0) return;
ChangeHealth(-dmg);
_onReceiveDamage.Invoke(damage);
if (enableDebug)
Debug.Log($"[SummonDamageReceiver] {gameObject.name} took {dmg}, HP {currentHealth}/{maxHealth}");
}
public void AddHealth(int value)
{
if (isDead) return;
currentHealth = Mathf.Min(maxHealth, currentHealth + value);
}
public void ChangeHealth(int value)
{
if (isDead) return;
currentHealth = Mathf.Clamp(currentHealth + value, 0f, maxHealth);
if (currentHealth <= 0f)
Die();
}
public void ChangeMaxHealth(int value)
{
maxHealth = Mathf.Max(1, maxHealth + value);
currentHealth = Mathf.Min(currentHealth, maxHealth);
}
public void ResetHealth(float health)
{
maxHealth = Mathf.Max(1, maxHealth);
currentHealth = Mathf.Clamp(health, 0f, maxHealth);
isDead = false;
}
public void ResetHealth()
{
maxHealth = Mathf.Max(1, maxHealth);
currentHealth = maxHealth;
isDead = false;
}
private void Die()
{
if (isDead) return;
isDead = true;
_onDead.Invoke(gameObject);
if (deathEffect != null)
{
GameObject fx = Instantiate(deathEffect, transform.position, transform.rotation);
Destroy(fx, 5f);
}
if (destroyOnDeath)
Destroy(gameObject, destroyDelay);
}
}

View File

@@ -284,7 +284,7 @@ Animator:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2274584332873493075}
m_Enabled: 1
m_Enabled: 0
m_Avatar: {fileID: 9000000, guid: 0120314ed622daf4f829420bf369f48d, type: 3}
m_Controller: {fileID: 9100000, guid: f1afa426b318a544da0ef42f7fe15542, type: 2}
m_CullingMode: 1
@@ -936,7 +936,7 @@ MonoBehaviour:
openCloseWindow: 1
selectedToolbar: 0
_fsmBehaviour: {fileID: 11400000, guid: 73af8a7d97f5f714d819d2d2f73aa449, type: 2}
_stop: 0
_stop: 1
_debugMode: 0
onStartFSM:
m_PersistentCalls:

View File

@@ -166,7 +166,7 @@ SkinnedMeshRenderer:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 119298518639331252}
m_Enabled: 1
m_Enabled: 0
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
@@ -21859,7 +21859,7 @@ MonoBehaviour:
openCloseWindow: 1
selectedToolbar: 0
_fsmBehaviour: {fileID: 11400000, guid: 478bc6a7e9fd82c4a9d395af400e05e6, type: 2}
_stop: 0
_stop: 1
_debugMode: 0
onStartFSM:
m_PersistentCalls:

View File

@@ -4533,7 +4533,7 @@ ParticleSystemRenderer:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6585598795937982}
m_Enabled: 1
m_Enabled: 0
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
@@ -13371,7 +13371,7 @@ MonoBehaviour:
openCloseWindow: 1
selectedToolbar: 0
_fsmBehaviour: {fileID: 11400000, guid: d9cf652bbbb9e0c40b58b5d59170055a, type: 2}
_stop: 0
_stop: 1
_debugMode: 0
onStartFSM:
m_PersistentCalls:

View File

@@ -103,7 +103,7 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 69635961216777874}
m_Enabled: 1
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1b04f4ac1b776ff48b5f78a97b57f776, type: 3}
m_Name:
@@ -30742,7 +30742,7 @@ MonoBehaviour:
openCloseWindow: 1
selectedToolbar: 0
_fsmBehaviour: {fileID: 11400000, guid: 73af8a7d97f5f714d819d2d2f73aa449, type: 2}
_stop: 0
_stop: 1
_debugMode: 0
onStartFSM:
m_PersistentCalls:

View File

@@ -39,7 +39,7 @@ SkinnedMeshRenderer:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 226257558141750692}
m_Enabled: 1
m_Enabled: 0
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
@@ -6368,7 +6368,7 @@ MonoBehaviour:
openCloseWindow: 1
selectedToolbar: 0
_fsmBehaviour: {fileID: 11400000, guid: 478bc6a7e9fd82c4a9d395af400e05e6, type: 2}
_stop: 0
_stop: 1
_debugMode: 0
onStartFSM:
m_PersistentCalls:

View File

@@ -4753,7 +4753,7 @@ ParticleSystemRenderer:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 644455550314575239}
m_Enabled: 1
m_Enabled: 0
m_CastShadows: 0
m_ReceiveShadows: 1
m_DynamicOccludee: 1
@@ -38453,7 +38453,7 @@ MonoBehaviour:
openCloseWindow: 1
selectedToolbar: 0
_fsmBehaviour: {fileID: 11400000, guid: 478bc6a7e9fd82c4a9d395af400e05e6, type: 2}
_stop: 0
_stop: 1
_debugMode: 0
onStartFSM:
m_PersistentCalls:

View File

@@ -103,7 +103,7 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 466729051031186190}
m_Enabled: 1
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fb97f58541b05b24ab37661346298ff2, type: 3}
m_Name:
@@ -1005,7 +1005,7 @@ MonoBehaviour:
openCloseWindow: 1
selectedToolbar: 0
_fsmBehaviour: {fileID: 0}
_stop: 0
_stop: 1
_debugMode: 0
onStartFSM:
m_PersistentCalls:

View File

@@ -198,7 +198,7 @@ CapsuleCollider:
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
m_Enabled: 0
serializedVersion: 2
m_Radius: 1
m_Height: 4.25
@@ -758,7 +758,7 @@ MonoBehaviour:
openCloseWindow: 1
selectedToolbar: 0
_fsmBehaviour: {fileID: 11400000, guid: 478bc6a7e9fd82c4a9d395af400e05e6, type: 2}
_stop: 0
_stop: 1
_debugMode: 0
onStartFSM:
m_PersistentCalls:

View File

@@ -0,0 +1,378 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using Invector.vMelee;
public class SwarmAgent : MonoBehaviour
{
public enum SwarmProfile
{
Default,
Spider,
Wolf,
Dragonfly
}
private enum SwarmState
{
Idle,
Chase,
Attack,
Retreat
}
[Header("Profile")]
[SerializeField] private bool autoProfileByName = true;
[SerializeField] private SwarmProfile profileOverride = SwarmProfile.Default;
[Header("Targeting")]
[SerializeField] private SwarmCoordinator coordinator;
[SerializeField] private string playerTag = "Player";
[SerializeField] private float targetRefreshInterval = 1.0f;
[Header("Movement")]
[SerializeField] private float moveSpeed = 3.5f;
[SerializeField] private float acceleration = 8f;
[SerializeField] private float angularSpeed = 360f;
[SerializeField] private float stoppingDistance = 1.6f;
[SerializeField] private float repathInterval = 0.25f;
[Header("Attack")]
[SerializeField] private float attackRange = 1.8f;
[SerializeField] private Vector2Int attacksPerBurst = new Vector2Int(1, 2);
[SerializeField] private float attackCooldown = 0.8f;
[SerializeField] private float damageEnableDelay = 0.1f;
[SerializeField] private float damageWindow = 0.25f;
[SerializeField] private string[] attackTriggers = new string[] { "Attack" };
[Header("Retreat")]
[SerializeField] private float retreatDuration = 1.25f;
[SerializeField] private float retreatDistance = 3.5f;
[Header("Spider Strafe")]
[SerializeField] private float strafeRadius = 2.5f;
[SerializeField] private float strafeSpeed = 2.0f;
[Header("Wolf Flank")]
[SerializeField] private float flankDistance = 2.5f;
[SerializeField] private float flankSideOffset = 2.0f;
[SerializeField] private float flankReplanInterval = 0.6f;
[Header("Dragonfly Figure 8")]
[SerializeField] private float figureEightRadius = 3.0f;
[SerializeField] private float figureEightSpeed = 2.2f;
private SwarmProfile profile;
private SwarmState state = SwarmState.Idle;
private NavMeshAgent agent;
private Animator animator;
private vMeleeAttackObject[] meleeAttackObjects;
private Transform target;
private float nextTargetRefreshTime;
private float nextRepathTime;
private float nextTickTime;
private float lastAttackTime;
private float retreatEndTime;
private float flankSide = 1f;
private float nextFlankReplanTime;
private Vector3 retreatCenter;
private float retreatStartTime;
private Coroutine attackRoutine;
private void Awake()
{
agent = GetComponent<NavMeshAgent>();
animator = GetComponentInChildren<Animator>();
meleeAttackObjects = GetComponentsInChildren<vMeleeAttackObject>(true);
if (coordinator == null)
coordinator = GetComponentInParent<SwarmCoordinator>();
if (autoProfileByName)
profile = ResolveProfileByName(gameObject.name);
else
profile = profileOverride;
}
private void OnEnable()
{
if (coordinator != null) coordinator.Register(this);
DisableDamage();
ConfigureAgent();
}
private void OnDisable()
{
if (coordinator != null) coordinator.Unregister(this);
}
private void Update()
{
if (Time.time < nextTickTime) return;
nextTickTime = Time.time + GetUpdateInterval();
Tick();
}
private void Tick()
{
RefreshTargetIfNeeded();
if (target == null)
{
state = SwarmState.Idle;
if (agent != null) agent.isStopped = true;
return;
}
if (state == SwarmState.Retreat)
{
UpdateRetreat();
return;
}
if (state == SwarmState.Attack)
return;
float sqrDist = (target.position - transform.position).sqrMagnitude;
float attackRangeSqr = attackRange * attackRange;
if (sqrDist <= attackRangeSqr && Time.time >= lastAttackTime + attackCooldown)
{
StartAttackBurst();
return;
}
UpdateChase();
}
private void UpdateChase()
{
if (agent == null) return;
state = SwarmState.Chase;
agent.isStopped = false;
if (Time.time < nextRepathTime) return;
nextRepathTime = Time.time + repathInterval;
Vector3 desired = GetDesiredPosition();
agent.SetDestination(desired);
}
private void StartAttackBurst()
{
if (attackRoutine != null) StopCoroutine(attackRoutine);
attackRoutine = StartCoroutine(AttackBurstCoroutine());
}
private IEnumerator AttackBurstCoroutine()
{
state = SwarmState.Attack;
lastAttackTime = Time.time;
if (agent != null) agent.isStopped = true;
int minAttacks = Mathf.Max(1, attacksPerBurst.x);
int maxAttacks = Mathf.Max(minAttacks, attacksPerBurst.y);
int count = Random.Range(minAttacks, maxAttacks + 1);
for (int i = 0; i < count; i++)
{
TriggerAttackAnimation();
yield return StartCoroutine(DamageWindowCoroutine());
yield return new WaitForSeconds(attackCooldown);
}
StartRetreat();
}
private IEnumerator DamageWindowCoroutine()
{
if (damageEnableDelay > 0f)
yield return new WaitForSeconds(damageEnableDelay);
EnableDamage();
if (damageWindow > 0f)
yield return new WaitForSeconds(damageWindow);
DisableDamage();
}
private void StartRetreat()
{
state = SwarmState.Retreat;
retreatStartTime = Time.time;
retreatEndTime = retreatStartTime + retreatDuration;
if (target != null)
retreatCenter = target.position;
else
retreatCenter = transform.position;
if (agent != null) agent.isStopped = false;
}
private void UpdateRetreat()
{
if (agent == null) return;
if (Time.time >= retreatEndTime)
{
state = SwarmState.Chase;
return;
}
if (Time.time < nextRepathTime) return;
nextRepathTime = Time.time + repathInterval;
Vector3 retreatTarget;
if (profile == SwarmProfile.Dragonfly)
{
float t = (Time.time - retreatStartTime) * figureEightSpeed;
float x = Mathf.Sin(t);
float z = Mathf.Sin(t * 2f) * 0.5f;
Vector3 offset = new Vector3(x, 0f, z) * figureEightRadius;
retreatTarget = retreatCenter + offset;
}
else
{
Vector3 away = (transform.position - target.position).normalized;
retreatTarget = transform.position + away * retreatDistance;
}
agent.SetDestination(retreatTarget);
}
private Vector3 GetDesiredPosition()
{
Vector3 baseTarget = target.position;
Vector3 separation = GetSeparationOffset();
switch (profile)
{
case SwarmProfile.Spider:
return baseTarget + GetStrafeOffset() + separation;
case SwarmProfile.Wolf:
return GetFlankPosition() + separation;
case SwarmProfile.Dragonfly:
return baseTarget + separation;
default:
return baseTarget + separation;
}
}
private Vector3 GetSeparationOffset()
{
if (coordinator == null) return Vector3.zero;
float radius = coordinator.SeparationRadius;
if (radius <= 0f) return Vector3.zero;
List<SwarmAgent> agents = coordinator.GetAgents();
if (agents == null || agents.Count == 0) return Vector3.zero;
Vector3 offset = Vector3.zero;
float radiusSqr = radius * radius;
Vector3 pos = transform.position;
for (int i = 0; i < agents.Count; i++)
{
SwarmAgent other = agents[i];
if (other == null || other == this) continue;
Vector3 delta = pos - other.transform.position;
float distSqr = delta.sqrMagnitude;
if (distSqr > 0f && distSqr < radiusSqr)
offset += delta.normalized * (1f - (distSqr / radiusSqr));
}
return offset * coordinator.SeparationWeight;
}
private Vector3 GetStrafeOffset()
{
Vector3 toTarget = (target.position - transform.position).normalized;
Vector3 right = new Vector3(-toTarget.z, 0f, toTarget.x);
float wave = Mathf.Sin(Time.time * strafeSpeed);
return right * (wave * strafeRadius);
}
private Vector3 GetFlankPosition()
{
if (Time.time >= nextFlankReplanTime)
{
nextFlankReplanTime = Time.time + flankReplanInterval;
flankSide = Random.value < 0.5f ? -1f : 1f;
}
Vector3 behind = -target.forward * flankDistance;
Vector3 side = target.right * (flankSide * flankSideOffset);
Vector3 desired = target.position + behind + side;
return desired;
}
private void ConfigureAgent()
{
if (agent == null) return;
agent.speed = moveSpeed;
agent.acceleration = acceleration;
agent.angularSpeed = angularSpeed;
agent.stoppingDistance = stoppingDistance;
}
private void RefreshTargetIfNeeded()
{
if (coordinator != null && coordinator.Target != null)
{
target = coordinator.Target;
return;
}
if (Time.time < nextTargetRefreshTime) return;
nextTargetRefreshTime = Time.time + targetRefreshInterval;
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
if (player != null) target = player.transform;
}
private void TriggerAttackAnimation()
{
if (animator == null || attackTriggers == null || attackTriggers.Length == 0) return;
string trigger = attackTriggers[Random.Range(0, attackTriggers.Length)];
if (!string.IsNullOrEmpty(trigger))
animator.SetTrigger(trigger);
}
private void EnableDamage()
{
for (int i = 0; i < meleeAttackObjects.Length; i++)
{
if (meleeAttackObjects[i] != null)
meleeAttackObjects[i].SetActiveDamage(true);
}
}
private void DisableDamage()
{
for (int i = 0; i < meleeAttackObjects.Length; i++)
{
if (meleeAttackObjects[i] != null)
meleeAttackObjects[i].SetActiveDamage(false);
}
}
private float GetUpdateInterval()
{
if (coordinator == null) return 0.2f;
float jitter = coordinator.UpdateJitter;
return coordinator.UpdateInterval + (jitter > 0f ? Random.Range(-jitter, jitter) : 0f);
}
private SwarmProfile ResolveProfileByName(string objectName)
{
string nameLower = objectName.ToLowerInvariant();
if (nameLower.Contains("spider")) return SwarmProfile.Spider;
if (nameLower.Contains("dog") || nameLower.Contains("barghest") || nameLower.Contains("golem"))
return SwarmProfile.Wolf;
if (nameLower.Contains("bug") || nameLower.Contains("flying"))
return SwarmProfile.Dragonfly;
return SwarmProfile.Default;
}
}

View File

@@ -0,0 +1,56 @@
using System.Collections.Generic;
using UnityEngine;
public class SwarmCoordinator : MonoBehaviour
{
[Header("Targeting")]
[SerializeField] private bool autoFindPlayer = true;
[SerializeField] private string playerTag = "Player";
[SerializeField] private float targetRefreshInterval = 0.75f;
[Header("Swarm Tuning")]
[SerializeField] private float updateInterval = 0.2f;
[SerializeField] private float updateJitter = 0.05f;
[SerializeField] private float separationRadius = 1.2f;
[SerializeField] private float separationWeight = 1.0f;
private readonly List<SwarmAgent> agents = new List<SwarmAgent>();
private Transform target;
private float nextTargetRefreshTime;
public Transform Target => target;
public float UpdateInterval => updateInterval;
public float UpdateJitter => updateJitter;
public float SeparationRadius => separationRadius;
public float SeparationWeight => separationWeight;
private void Update()
{
if (!autoFindPlayer) return;
if (Time.time < nextTargetRefreshTime) return;
nextTargetRefreshTime = Time.time + targetRefreshInterval;
if (target == null)
{
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
if (player != null) target = player.transform;
}
}
public void Register(SwarmAgent agent)
{
if (agent == null || agents.Contains(agent)) return;
agents.Add(agent);
}
public void Unregister(SwarmAgent agent)
{
if (agent == null) return;
agents.Remove(agent);
}
public List<SwarmAgent> GetAgents()
{
return agents;
}
}