385 lines
15 KiB
C#
385 lines
15 KiB
C#
using System;
|
|
using RaceFields.Vehicles;
|
|
using UnityEngine;
|
|
|
|
namespace RaceFields.Damage.VehicleDamage
|
|
{
|
|
[RequireComponent(typeof(Rigidbody))]
|
|
[RequireComponent(typeof(ArcadeBuggyController))]
|
|
public sealed class VehicleDamageController : MonoBehaviour
|
|
{
|
|
[Serializable]
|
|
public sealed class VehicleModule
|
|
{
|
|
[SerializeField] private VehicleModuleType moduleType = VehicleModuleType.Body;
|
|
[SerializeField] private string displayName = "Module";
|
|
[SerializeField] private float maxHealth = 100f;
|
|
[SerializeField, Range(0f, 1f)] private float damagedHealthRatio = 0.5f;
|
|
[SerializeField] private WheelSlot wheelSlot;
|
|
[SerializeField] private bool detachWheelVisualOnDestroyed;
|
|
[SerializeField] private float detachImpulse = 2f;
|
|
[SerializeField] private float currentHealth;
|
|
[SerializeField] private VehicleModuleState state = VehicleModuleState.Healthy;
|
|
|
|
public VehicleModuleType ModuleType => moduleType;
|
|
public string DisplayName => displayName;
|
|
public float MaxHealth => maxHealth;
|
|
public float CurrentHealth => currentHealth;
|
|
public VehicleModuleState State => state;
|
|
public WheelSlot WheelSlot => wheelSlot;
|
|
public bool DetachWheelVisualOnDestroyed => detachWheelVisualOnDestroyed;
|
|
public float DetachImpulse => detachImpulse;
|
|
public float NormalizedHealth => maxHealth <= 0f ? 0f : Mathf.Clamp01(currentHealth / maxHealth);
|
|
|
|
public VehicleModule(VehicleModuleType moduleType, string displayName, float maxHealth)
|
|
{
|
|
this.moduleType = moduleType;
|
|
this.displayName = displayName;
|
|
this.maxHealth = maxHealth;
|
|
}
|
|
|
|
public VehicleModule WithWheel(WheelSlot slot, bool detachVisual)
|
|
{
|
|
wheelSlot = slot;
|
|
detachWheelVisualOnDestroyed = detachVisual;
|
|
return this;
|
|
}
|
|
|
|
public void ResetHealth()
|
|
{
|
|
currentHealth = Mathf.Max(1f, maxHealth);
|
|
state = VehicleModuleState.Healthy;
|
|
}
|
|
|
|
public bool ApplyDamage(float amount)
|
|
{
|
|
if (state == VehicleModuleState.Destroyed || state == VehicleModuleState.Detached)
|
|
return false;
|
|
|
|
VehicleModuleState previous = state;
|
|
currentHealth = Mathf.Max(0f, currentHealth - Mathf.Max(0f, amount));
|
|
state = ResolveState();
|
|
return previous != state;
|
|
}
|
|
|
|
public void MarkDetached()
|
|
{
|
|
currentHealth = 0f;
|
|
state = VehicleModuleState.Detached;
|
|
}
|
|
|
|
private VehicleModuleState ResolveState()
|
|
{
|
|
if (currentHealth <= 0f)
|
|
return VehicleModuleState.Destroyed;
|
|
|
|
return NormalizedHealth <= damagedHealthRatio
|
|
? VehicleModuleState.Damaged
|
|
: VehicleModuleState.Healthy;
|
|
}
|
|
}
|
|
|
|
[Header("Modules")]
|
|
[SerializeField] private VehicleModule[] moduleHealth;
|
|
|
|
[Header("Startup")]
|
|
[SerializeField] private float spawnGraceTime = 1f;
|
|
|
|
[Header("Collision Damage")]
|
|
[SerializeField] private float minCollisionSpeed = 11f;
|
|
[SerializeField] private float collisionDamagePerSpeed = 4.5f;
|
|
[SerializeField] private float maxCollisionDamage = 95f;
|
|
[SerializeField] private float collisionCooldown = 0.25f;
|
|
|
|
[Header("Landing Damage")]
|
|
[SerializeField] private float minAirTimeForLandingDamage = 0.45f;
|
|
[SerializeField] private float minLandingDownSpeed = 9f;
|
|
[SerializeField] private float landingDamagePerSpeed = 4f;
|
|
[SerializeField] private float maxLandingDamage = 75f;
|
|
[SerializeField, Range(0f, 1f)] private float landingWheelDamageShare = 0.65f;
|
|
[SerializeField, Range(0f, 1f)] private float landingBodyDamageShare = 0.35f;
|
|
|
|
[Header("Debug")]
|
|
[SerializeField] private bool logDamage;
|
|
[SerializeField] private string lastDamageDebug;
|
|
|
|
public event Action<VehicleModule> ModuleStateChanged;
|
|
|
|
public VehicleModule[] Modules => moduleHealth;
|
|
public string LastDamageDebug => lastDamageDebug;
|
|
|
|
private ArcadeBuggyController buggyController;
|
|
private Rigidbody body;
|
|
private float ignoreDamageUntil;
|
|
private float nextCollisionDamageTime;
|
|
private WheelImpactState frontLeftState;
|
|
private WheelImpactState frontRightState;
|
|
private WheelImpactState rearLeftState;
|
|
private WheelImpactState rearRightState;
|
|
|
|
private struct WheelImpactState
|
|
{
|
|
public bool WasGrounded;
|
|
public float AirborneStartTime;
|
|
public float MaxDownSpeed;
|
|
}
|
|
|
|
private void Reset()
|
|
{
|
|
moduleHealth = CreateDefaultModules();
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
buggyController = GetComponent<ArcadeBuggyController>();
|
|
body = GetComponent<Rigidbody>();
|
|
EnsureModules();
|
|
ResetModuleHealth();
|
|
ignoreDamageUntil = Time.time + spawnGraceTime;
|
|
frontLeftState.WasGrounded = true;
|
|
frontRightState.WasGrounded = true;
|
|
rearLeftState.WasGrounded = true;
|
|
rearRightState.WasGrounded = true;
|
|
}
|
|
|
|
private void FixedUpdate()
|
|
{
|
|
if (Time.time < ignoreDamageUntil)
|
|
return;
|
|
|
|
UpdateWheelLanding(WheelSlot.FrontLeft, VehicleModuleType.FrontLeftWheel, ref frontLeftState);
|
|
UpdateWheelLanding(WheelSlot.FrontRight, VehicleModuleType.FrontRightWheel, ref frontRightState);
|
|
UpdateWheelLanding(WheelSlot.RearLeft, VehicleModuleType.RearLeftWheel, ref rearLeftState);
|
|
UpdateWheelLanding(WheelSlot.RearRight, VehicleModuleType.RearRightWheel, ref rearRightState);
|
|
}
|
|
|
|
private void OnCollisionEnter(Collision collision)
|
|
{
|
|
if (Time.time < ignoreDamageUntil || Time.time < nextCollisionDamageTime)
|
|
return;
|
|
|
|
if (collision.contactCount == 0)
|
|
return;
|
|
|
|
ContactPoint contact = collision.GetContact(0);
|
|
float impactSpeed = Mathf.Abs(Vector3.Dot(collision.relativeVelocity, contact.normal));
|
|
if (impactSpeed < minCollisionSpeed)
|
|
return;
|
|
|
|
VehicleModuleType moduleType = ResolveImpactModule(contact.point);
|
|
float damage = Mathf.Min(maxCollisionDamage, (impactSpeed - minCollisionSpeed) * collisionDamagePerSpeed);
|
|
ApplyDamage(moduleType, new DamageHit(damage, DamageType.Collision, contact.point, collision.relativeVelocity.normalized, gameObject));
|
|
nextCollisionDamageTime = Time.time + collisionCooldown;
|
|
}
|
|
|
|
public void ApplyDamage(VehicleModuleType moduleType, DamageHit hit)
|
|
{
|
|
VehicleModule module = GetModule(moduleType);
|
|
if (module == null)
|
|
return;
|
|
|
|
bool changed = module.ApplyDamage(hit.Amount);
|
|
lastDamageDebug = $"{hit.Type}: {hit.Amount:F1} -> {moduleType}";
|
|
|
|
if (module.State == VehicleModuleState.Destroyed && IsWheelModule(module.ModuleType))
|
|
DisableDestroyedWheel(module);
|
|
|
|
if (logDamage)
|
|
Debug.Log($"{name} {lastDamageDebug}", this);
|
|
|
|
if (changed)
|
|
ModuleStateChanged?.Invoke(module);
|
|
}
|
|
|
|
public VehicleModule GetModule(VehicleModuleType moduleType)
|
|
{
|
|
EnsureModules();
|
|
|
|
for (int i = 0; i < moduleHealth.Length; i++)
|
|
{
|
|
if (moduleHealth[i] != null && moduleHealth[i].ModuleType == moduleType)
|
|
return moduleHealth[i];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public VehicleModuleState GetModuleState(VehicleModuleType moduleType)
|
|
{
|
|
VehicleModule module = GetModule(moduleType);
|
|
return module != null ? module.State : VehicleModuleState.Healthy;
|
|
}
|
|
|
|
public void RepairAll()
|
|
{
|
|
EnsureModules();
|
|
|
|
for (int i = 0; i < moduleHealth.Length; i++)
|
|
{
|
|
if (moduleHealth[i] == null)
|
|
continue;
|
|
|
|
moduleHealth[i].ResetHealth();
|
|
ModuleStateChanged?.Invoke(moduleHealth[i]);
|
|
}
|
|
}
|
|
|
|
private void UpdateWheelLanding(WheelSlot slot, VehicleModuleType moduleType, ref WheelImpactState state)
|
|
{
|
|
WheelCollider wheel = buggyController.GetWheelCollider(slot);
|
|
if (wheel == null || !wheel.enabled)
|
|
return;
|
|
|
|
bool grounded = wheel.isGrounded;
|
|
if (!grounded)
|
|
{
|
|
if (state.WasGrounded)
|
|
{
|
|
state.AirborneStartTime = Time.time;
|
|
state.MaxDownSpeed = 0f;
|
|
}
|
|
|
|
state.WasGrounded = false;
|
|
Vector3 gravityDirection = Physics.gravity.sqrMagnitude > 0f ? -Physics.gravity.normalized : Vector3.down;
|
|
float downSpeed = Vector3.Dot(body.GetPointVelocity(wheel.transform.position), gravityDirection);
|
|
state.MaxDownSpeed = Mathf.Max(state.MaxDownSpeed, Mathf.Max(0f, downSpeed));
|
|
return;
|
|
}
|
|
|
|
if (state.WasGrounded)
|
|
return;
|
|
|
|
float airTime = Time.time - state.AirborneStartTime;
|
|
float maxDownSpeed = state.MaxDownSpeed;
|
|
state.WasGrounded = true;
|
|
state.AirborneStartTime = 0f;
|
|
state.MaxDownSpeed = 0f;
|
|
|
|
if (airTime < minAirTimeForLandingDamage || maxDownSpeed < minLandingDownSpeed)
|
|
return;
|
|
|
|
float totalDamage = Mathf.Min(maxLandingDamage, (maxDownSpeed - minLandingDownSpeed) * landingDamagePerSpeed);
|
|
ApplyDamage(moduleType, new DamageHit(totalDamage * landingWheelDamageShare, DamageType.Collision, wheel.transform.position, Vector3.down, gameObject));
|
|
|
|
float bodyDamage = totalDamage * landingBodyDamageShare * 0.25f;
|
|
if (bodyDamage > 0f)
|
|
ApplyDamage(VehicleModuleType.Body, new DamageHit(bodyDamage, DamageType.Collision, wheel.transform.position, Vector3.down, gameObject));
|
|
}
|
|
|
|
private VehicleModuleType ResolveImpactModule(Vector3 worldPoint)
|
|
{
|
|
if (TryResolveWheelImpact(worldPoint, out WheelSlot closestWheel))
|
|
return WheelSlotToModule(closestWheel);
|
|
|
|
Vector3 localPoint = transform.InverseTransformPoint(worldPoint);
|
|
return localPoint.z > 0.65f ? VehicleModuleType.Engine : VehicleModuleType.Body;
|
|
}
|
|
|
|
private bool TryResolveWheelImpact(Vector3 worldPoint, out WheelSlot closestWheel)
|
|
{
|
|
closestWheel = WheelSlot.FrontLeft;
|
|
float bestDistance = float.MaxValue;
|
|
bool found = false;
|
|
|
|
CheckWheelDistance(WheelSlot.FrontLeft, worldPoint, ref closestWheel, ref bestDistance, ref found);
|
|
CheckWheelDistance(WheelSlot.FrontRight, worldPoint, ref closestWheel, ref bestDistance, ref found);
|
|
CheckWheelDistance(WheelSlot.RearLeft, worldPoint, ref closestWheel, ref bestDistance, ref found);
|
|
CheckWheelDistance(WheelSlot.RearRight, worldPoint, ref closestWheel, ref bestDistance, ref found);
|
|
|
|
return found;
|
|
}
|
|
|
|
private void CheckWheelDistance(WheelSlot slot, Vector3 worldPoint, ref WheelSlot closestWheel, ref float bestDistance, ref bool found)
|
|
{
|
|
WheelCollider wheel = buggyController.GetWheelCollider(slot);
|
|
if (wheel == null || !wheel.enabled)
|
|
return;
|
|
|
|
float limit = Mathf.Max(0.45f, wheel.radius * 1.8f);
|
|
float distance = Vector3.Distance(worldPoint, wheel.transform.position);
|
|
if (distance > limit || distance >= bestDistance)
|
|
return;
|
|
|
|
closestWheel = slot;
|
|
bestDistance = distance;
|
|
found = true;
|
|
}
|
|
|
|
private void DisableDestroyedWheel(VehicleModule module)
|
|
{
|
|
buggyController.SetWheelEnabled(module.WheelSlot, false);
|
|
|
|
if (!module.DetachWheelVisualOnDestroyed)
|
|
return;
|
|
|
|
Transform visual = buggyController.GetWheelVisual(module.WheelSlot);
|
|
if (visual == null)
|
|
return;
|
|
|
|
visual.SetParent(null, true);
|
|
|
|
Collider wheelCollider = visual.GetComponent<Collider>();
|
|
if (wheelCollider == null)
|
|
wheelCollider = visual.gameObject.AddComponent<SphereCollider>();
|
|
|
|
Rigidbody wheelBody = visual.GetComponent<Rigidbody>();
|
|
if (wheelBody == null)
|
|
wheelBody = visual.gameObject.AddComponent<Rigidbody>();
|
|
|
|
wheelBody.mass = 18f;
|
|
wheelBody.interpolation = RigidbodyInterpolation.Interpolate;
|
|
wheelBody.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
|
|
wheelBody.AddForce((visual.right + Vector3.up * 0.35f) * module.DetachImpulse, ForceMode.Impulse);
|
|
module.MarkDetached();
|
|
}
|
|
|
|
private void EnsureModules()
|
|
{
|
|
if (moduleHealth == null || moduleHealth.Length == 0)
|
|
moduleHealth = CreateDefaultModules();
|
|
}
|
|
|
|
private void ResetModuleHealth()
|
|
{
|
|
for (int i = 0; i < moduleHealth.Length; i++)
|
|
{
|
|
if (moduleHealth[i] != null)
|
|
moduleHealth[i].ResetHealth();
|
|
}
|
|
}
|
|
|
|
private static VehicleModule[] CreateDefaultModules()
|
|
{
|
|
return new[]
|
|
{
|
|
new VehicleModule(VehicleModuleType.Body, "Body", 180f),
|
|
new VehicleModule(VehicleModuleType.Engine, "Engine", 120f),
|
|
new VehicleModule(VehicleModuleType.FrontLeftWheel, "Front Left Wheel", 70f).WithWheel(WheelSlot.FrontLeft, true),
|
|
new VehicleModule(VehicleModuleType.FrontRightWheel, "Front Right Wheel", 70f).WithWheel(WheelSlot.FrontRight, true),
|
|
new VehicleModule(VehicleModuleType.RearLeftWheel, "Rear Left Wheel", 70f).WithWheel(WheelSlot.RearLeft, true),
|
|
new VehicleModule(VehicleModuleType.RearRightWheel, "Rear Right Wheel", 70f).WithWheel(WheelSlot.RearRight, true)
|
|
};
|
|
}
|
|
|
|
private static bool IsWheelModule(VehicleModuleType moduleType)
|
|
{
|
|
return moduleType == VehicleModuleType.FrontLeftWheel ||
|
|
moduleType == VehicleModuleType.FrontRightWheel ||
|
|
moduleType == VehicleModuleType.RearLeftWheel ||
|
|
moduleType == VehicleModuleType.RearRightWheel;
|
|
}
|
|
|
|
private static VehicleModuleType WheelSlotToModule(WheelSlot slot)
|
|
{
|
|
return slot switch
|
|
{
|
|
WheelSlot.FrontLeft => VehicleModuleType.FrontLeftWheel,
|
|
WheelSlot.FrontRight => VehicleModuleType.FrontRightWheel,
|
|
WheelSlot.RearLeft => VehicleModuleType.RearLeftWheel,
|
|
WheelSlot.RearRight => VehicleModuleType.RearRightWheel,
|
|
_ => VehicleModuleType.Body
|
|
};
|
|
}
|
|
}
|
|
}
|