Files
megakoop/Game/Scripts/WeaponController.cs
2025-10-05 18:21:16 +02:00

535 lines
17 KiB
C#

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<WeaponRuntime> equippedWeapons = new();
private readonly HashSet<Collider> ignoredOwnerColliders = new();
public event Action<ProjectileSpawnEvent> ProjectileSpawned;
private float maxWeaponRange;
private float retargetTimer;
private TargetInfo currentTarget;
private bool hasTarget;
private Collider[] ownerColliders;
private Health ownerHealth;
private void Awake()
{
ownerHealth = GetComponent<Health>();
if (ownerHealth != null)
{
ownerTeam = ownerHealth.Team;
}
ownerColliders = GetComponentsInChildren<Collider>();
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 (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 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;
}
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<IDamageable>();
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);
projectile.Initialize(direction, weapon.Definition.ProjectileSpeed, weapon.Definition.Damage, weapon.Definition.ProjectileLifetime, gameObject, ownerTeam, ownerColliders, weapon.Definition.HitMask);
ProjectileSpawned?.Invoke(new ProjectileSpawnEvent(weapon.Index, muzzle.position, direction, weapon.Definition.ProjectileSpeed, weapon.Definition.ProjectileLifetime, weapon.Definition.Damage));
}
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<Transform>();
}
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);
}
}
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;
}
}
}
}