using System; using System.Collections.Generic; using MegaKoop.Game.Combat; using MegaKoop.Game.WeaponSystem; using UnityEngine; using Random = UnityEngine.Random; namespace MegaKoop.Game.WeaponSystem { [DisallowMultipleComponent] public class WeaponController : MonoBehaviour { [Header("Ownership")] [SerializeField] private Team ownerTeam = Team.Heroes; [SerializeField] private Transform weaponSocket; [Header("Targeting")] [SerializeField] private float acquisitionRadius = 30f; [SerializeField] private float retargetInterval = 0.25f; [SerializeField] private LayerMask targetMask = Physics.DefaultRaycastLayers; [SerializeField] private LayerMask lineOfSightMask = Physics.DefaultRaycastLayers; [SerializeField] private bool requireLineOfSight = true; [Header("Loadout")] [SerializeField] private WeaponDefinition[] startingWeapons; [SerializeField] private bool autoFire = true; private static readonly RaycastHit[] LineOfSightHitsBuffer = new RaycastHit[8]; private readonly List equippedWeapons = new(); private readonly HashSet ignoredOwnerColliders = new(); public event Action ProjectileSpawned; private float maxWeaponRange; private float retargetTimer; private TargetInfo currentTarget; private bool hasTarget; private Collider[] ownerColliders; private Health ownerHealth; private bool isLocallyControlled = true; private bool hasDamageOverride; private float damageOverrideValue = -1f; private void Awake() { ownerHealth = GetComponent(); if (ownerHealth != null) { ownerTeam = ownerHealth.Team; } ownerColliders = GetComponentsInChildren(); if (ownerColliders != null) { for (int i = 0; i < ownerColliders.Length; i++) { Collider ownerCollider = ownerColliders[i]; if (ownerCollider == null) { continue; } ignoredOwnerColliders.Add(ownerCollider); } } } private void Start() { if (weaponSocket == null) { weaponSocket = transform; } if (startingWeapons != null) { foreach (WeaponDefinition definition in startingWeapons) { Equip(definition, weaponSocket); } } } private void Update() { if (!isLocallyControlled) { return; } if (equippedWeapons.Count == 0) { return; } if (autoFire) { UpdateTargetSelection(Time.deltaTime); } TargetInfo target = hasTarget ? currentTarget : default; foreach (WeaponRuntime weapon in equippedWeapons) { weapon.Tick(Time.deltaTime, target, this); } } public void SetLocalAuthority(bool allowLocalControl) { if (isLocallyControlled == allowLocalControl) { return; } isLocallyControlled = allowLocalControl; if (!isLocallyControlled) { hasTarget = false; retargetTimer = 0f; } } public void SetDamageOverride(float damage) { if (damage > 0f) { damageOverrideValue = damage; hasDamageOverride = true; } else { ClearDamageOverride(); } } public void ClearDamageOverride() { hasDamageOverride = false; damageOverrideValue = -1f; } public void Equip(WeaponDefinition definition, Transform mount) { if (definition == null || mount == null) { return; } int weaponIndex = equippedWeapons.Count; WeaponRuntime runtime = new WeaponRuntime(definition, mount, weaponIndex); equippedWeapons.Add(runtime); maxWeaponRange = Mathf.Max(maxWeaponRange, definition.Range); } public void Unequip(WeaponDefinition definition) { if (definition == null) { return; } for (int i = equippedWeapons.Count - 1; i >= 0; i--) { if (equippedWeapons[i].Definition == definition) { equippedWeapons[i].Dispose(); equippedWeapons.RemoveAt(i); } } RecalculateMaxRange(); } private void OnDisable() { foreach (WeaponRuntime weapon in equippedWeapons) { weapon.Dispose(); } equippedWeapons.Clear(); hasTarget = false; ClearDamageOverride(); } private void UpdateTargetSelection(float deltaTime) { retargetTimer -= deltaTime; if (hasTarget) { if (!currentTarget.IsValid) { hasTarget = false; } else { float distance = Vector3.Distance(transform.position, currentTarget.Transform.position); float allowedRange = Mathf.Max(acquisitionRadius, maxWeaponRange); if (distance > allowedRange * 1.1f) { hasTarget = false; } } } if (retargetTimer > 0f && hasTarget) { return; } retargetTimer = retargetInterval; AcquireTarget(); } private void AcquireTarget() { float searchRadius = Mathf.Max(acquisitionRadius, maxWeaponRange); if (searchRadius <= 0f) { return; } Collider[] hits = Physics.OverlapSphere(transform.position, searchRadius, targetMask, QueryTriggerInteraction.Collide); TargetInfo bestTarget = default; float bestScore = float.MaxValue; foreach (Collider hit in hits) { if (hit == null) { continue; } if (ignoredOwnerColliders.Contains(hit)) { continue; } IDamageable damageable = hit.GetComponentInParent(); if (damageable == null || !damageable.IsAlive) { continue; } if (damageable.Team == ownerTeam && ownerTeam != Team.Neutral) { continue; } if (damageable is not Component component) { continue; } Transform targetTransform = component.transform; float sqrDistance = (targetTransform.position - transform.position).sqrMagnitude; if (sqrDistance > maxWeaponRange * maxWeaponRange) { continue; } float score = sqrDistance; if (score < bestScore) { bestScore = score; bestTarget = new TargetInfo(damageable, hit, targetTransform); } } hasTarget = bestTarget.IsValid; currentTarget = bestTarget; } private void RecalculateMaxRange() { maxWeaponRange = 0f; for (int i = 0; i < equippedWeapons.Count; i++) { WeaponRuntime weapon = equippedWeapons[i]; weapon.SetIndex(i); maxWeaponRange = Mathf.Max(maxWeaponRange, weapon.Definition.Range); } } private Vector3 GetAimPoint(TargetInfo target) { if (!target.IsValid) { return Vector3.zero; } if (target.Collider != null) { return target.Collider.bounds.center; } return target.Transform.position; } private bool HasLineOfSight(Transform muzzle, Vector3 aimPoint, TargetInfo target) { if (!requireLineOfSight) { return true; } Vector3 origin = muzzle.position; Vector3 toTarget = aimPoint - origin; float distance = toTarget.magnitude; if (distance <= 0.01f) { return true; } Vector3 direction = toTarget.normalized; int hitCount = Physics.RaycastNonAlloc(origin, direction, LineOfSightHitsBuffer, distance, lineOfSightMask, QueryTriggerInteraction.Ignore); if (hitCount == 0) { return true; } float targetDistance = float.MaxValue; bool hasTargetHit = false; for (int i = 0; i < hitCount; i++) { RaycastHit hit = LineOfSightHitsBuffer[i]; Collider hitCollider = hit.collider; if (hitCollider == null) { continue; } if (ignoredOwnerColliders.Contains(hitCollider)) { continue; } if (target.Collider != null && hitCollider == target.Collider) { targetDistance = hit.distance; hasTargetHit = true; continue; } if (hit.distance < targetDistance) { return false; } } return hasTargetHit; } private void SpawnProjectile(WeaponRuntime weapon, Transform muzzle, Vector3 direction) { Projectile projectilePrefab = weapon.Definition.ProjectilePrefab; if (projectilePrefab == null) { return; } Quaternion rotation = direction.sqrMagnitude > 0f ? Quaternion.LookRotation(direction) : muzzle.rotation; Projectile projectile = Instantiate(projectilePrefab, muzzle.position, rotation); float resolvedDamage = hasDamageOverride ? damageOverrideValue : weapon.Definition.Damage; projectile.Initialize(direction, weapon.Definition.ProjectileSpeed, resolvedDamage, weapon.Definition.ProjectileLifetime, gameObject, ownerTeam, ownerColliders, weapon.Definition.HitMask, true); ProjectileSpawned?.Invoke(new ProjectileSpawnEvent(weapon.Index, muzzle.position, direction, weapon.Definition.ProjectileSpeed, weapon.Definition.ProjectileLifetime, resolvedDamage)); } public void SpawnNetworkProjectile(int weaponIndex, Vector3 position, Vector3 direction, float speed, float lifetime, float damage) { if (weaponIndex < 0 || weaponIndex >= equippedWeapons.Count) { return; } WeaponRuntime weapon = equippedWeapons[weaponIndex]; weapon.SpawnFromNetwork(position, direction, speed, lifetime, damage, ownerTeam, ownerColliders, gameObject); } private void FireWeapon(WeaponRuntime weapon, TargetInfo target, Transform muzzle) { Vector3 aimPoint = GetAimPoint(target); Vector3 toTarget = aimPoint - muzzle.position; if (toTarget.sqrMagnitude <= 0.0001f) { toTarget = muzzle.forward; } if (!HasLineOfSight(muzzle, aimPoint, target)) { return; } Vector3 baseDirection = toTarget.normalized; int projectiles = weapon.Definition.ProjectilesPerShot; float spread = weapon.Definition.SpreadAngle; for (int i = 0; i < projectiles; i++) { Vector3 direction = baseDirection; if (spread > 0f) { Vector2 offset = Random.insideUnitCircle * spread; Quaternion spreadRotation = Quaternion.AngleAxis(offset.x, muzzle.up) * Quaternion.AngleAxis(offset.y, muzzle.right); direction = spreadRotation * baseDirection; } SpawnProjectile(weapon, muzzle, direction); } weapon.ResetCooldown(); } private readonly struct TargetInfo { public readonly IDamageable Damageable; public readonly Collider Collider; public readonly Transform Transform; public TargetInfo(IDamageable damageable, Collider collider, Transform transform) { Damageable = damageable; Collider = collider; Transform = transform; } public bool IsValid => Damageable != null && Damageable.IsAlive && Transform != null; } private sealed class WeaponRuntime { private readonly Transform mountPoint; private readonly Transform fallbackMuzzle; private readonly Transform[] muzzles; private int muzzleIndex; private float cooldown; public WeaponDefinition Definition { get; } public WeaponView ViewInstance { get; } public int Index { get; private set; } public WeaponRuntime(WeaponDefinition definition, Transform mount, int index) { Definition = definition ?? throw new ArgumentNullException(nameof(definition)); mountPoint = mount; Index = index; if (definition.ViewPrefab != null) { ViewInstance = UnityEngine.Object.Instantiate(definition.ViewPrefab, mountPoint); ViewInstance.transform.localPosition = Vector3.zero; ViewInstance.transform.localRotation = Quaternion.identity; ViewInstance.transform.localScale = Vector3.one; muzzles = ViewInstance.Muzzles; } else { ViewInstance = null; muzzles = Array.Empty(); } fallbackMuzzle = mountPoint; muzzleIndex = 0; cooldown = 0f; } public void Tick(float deltaTime, TargetInfo target, WeaponController controller) { cooldown = Mathf.Max(0f, cooldown - deltaTime); if (cooldown > 0f) { return; } if (!target.IsValid) { return; } Transform muzzle = GetMuzzle(); if (muzzle == null) { return; } Vector3 aimPoint = controller.GetAimPoint(target); Vector3 toTarget = aimPoint - muzzle.position; float sqrDistance = toTarget.sqrMagnitude; float maxRange = Definition.Range; if (sqrDistance > maxRange * maxRange) { return; } controller.FireWeapon(this, target, muzzle); } public Transform GetMuzzle() { if (muzzles != null && muzzles.Length > 0) { for (int i = 0; i < muzzles.Length; i++) { int current = (muzzleIndex + i) % muzzles.Length; Transform muzzle = muzzles[current]; if (muzzle != null) { muzzleIndex = (current + 1) % muzzles.Length; return muzzle; } } } return fallbackMuzzle; } public void ResetCooldown() { cooldown = Definition.FireInterval; } public void Dispose() { if (ViewInstance != null) { UnityEngine.Object.Destroy(ViewInstance.gameObject); } } public void SetIndex(int newIndex) { Index = newIndex; } public void SpawnFromNetwork(Vector3 position, Vector3 direction, float speed, float lifetime, float damage, Team ownerTeam, Collider[] ownerColliders, GameObject owner) { Projectile projectilePrefab = Definition.ProjectilePrefab; if (projectilePrefab == null) { return; } Quaternion rotation = direction.sqrMagnitude > 0f ? Quaternion.LookRotation(direction) : fallbackMuzzle.rotation; Projectile projectile = UnityEngine.Object.Instantiate(projectilePrefab, position, rotation); float resolvedSpeed = speed > 0f ? speed : Definition.ProjectileSpeed; float resolvedLifetime = lifetime > 0f ? lifetime : Definition.ProjectileLifetime; float resolvedDamage = damage > 0f ? damage : Definition.Damage; projectile.Initialize(direction, resolvedSpeed, resolvedDamage, resolvedLifetime, owner, ownerTeam, ownerColliders, Definition.HitMask, false); } } public readonly struct ProjectileSpawnEvent { public readonly int WeaponIndex; public readonly Vector3 Position; public readonly Vector3 Direction; public readonly float Speed; public readonly float Lifetime; public readonly float Damage; public ProjectileSpawnEvent(int weaponIndex, Vector3 position, Vector3 direction, float speed, float lifetime, float damage) { WeaponIndex = weaponIndex; Position = position; Direction = direction; Speed = speed; Lifetime = lifetime; Damage = damage; } } } }