2026-08-12 12:11:13 +03:00

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
};
}
}
}