610 lines
17 KiB
C#
610 lines
17 KiB
C#
using System;
|
|
using UnityEngine;
|
|
using static AffectingForcesManager;
|
|
using ShipHandling;
|
|
using Managers;
|
|
using Unity.Mathematics;
|
|
using FORGE3D;
|
|
using PrimeTween;
|
|
using log4net;
|
|
using System.Reflection;
|
|
using System.Collections.Generic;
|
|
using static ShipSound;
|
|
|
|
public class Ship : MonoBehaviour, IHUDOwner, IDamageable
|
|
{
|
|
|
|
private static ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
|
|
|
public event Action<float> BoostUpdated;
|
|
public event Action<float> LifeUpdated;
|
|
|
|
public int InstanceID { get; private set; }
|
|
public ShipProperties Props;
|
|
public ShipState State;
|
|
public ShipInput Input;
|
|
// Private variables
|
|
public CameraOperator CameraOperator;
|
|
public ParticleSystem BoostEffect;
|
|
public ParticleSystem GravityEffect;
|
|
public ParticleSystem JetFlameEffect;
|
|
public ParticleSystem SmokeTrailEffect;
|
|
public DamageNumberParticles DamageParticleEffect;
|
|
|
|
public MeshRenderer BodyMeshRenderer;
|
|
|
|
private F3DFXController _fireController;
|
|
private AffectingForcesManager _forceManager;
|
|
public Rigidbody _body;
|
|
// Saves the current input value for thrust
|
|
private bool _canBoost = true;
|
|
private HitDetection[] _tackleDetectors;
|
|
private bool _isCriticalTackle = false;
|
|
private bool _isTackled = false;
|
|
private float _tackledTime = 0f;
|
|
private float _lastTackleTime = 0f;
|
|
private readonly float _minHitDelay = 0.06f;
|
|
private float _lastHitTime = 0f;
|
|
// Upcoming zone change
|
|
private Zone newZone = Zone.NimbleZone;
|
|
private WeaponEffect equippedWeapon = WeaponEffect.None;
|
|
private Dictionary<ShipSound, ManageableAudio> sounds = new();
|
|
|
|
|
|
void Awake()
|
|
{
|
|
if (_forceManager == null)
|
|
{
|
|
_forceManager = GameObject.FindGameObjectWithTag("ForceManager").
|
|
GetComponent<AffectingForcesManager>();
|
|
}
|
|
_body = GetComponent<Rigidbody>();
|
|
_fireController = GetComponent<F3DFXController>();
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
InstanceID = gameObject.GetInstanceID();
|
|
State.BoostCapacity = Props.MaxBoostCapacity;
|
|
State.RemainingHealth = Props.MaximumHealth;
|
|
|
|
// Get manageable audio instances for the ships sounds
|
|
foreach (ShipSoundToName stn in Props.Audio.shipSounds)
|
|
{
|
|
ManageableAudio ma = AudioManager.G.GetLocalSound(stn.soundName, 1,
|
|
gameObject.transform);
|
|
sounds[stn.sound] = ma;
|
|
}
|
|
|
|
// Register the ship with the camera
|
|
CameraOperator.AddCharacter(gameObject);
|
|
|
|
// Connect the tackling/tackled logic to the ships detection components
|
|
_tackleDetectors = GetComponentsInChildren<HitDetection>();
|
|
foreach (HitDetection td in _tackleDetectors)
|
|
{
|
|
td.TackledResponse += TackledResponse;
|
|
td.TacklingResponse += TacklingResponse;
|
|
td.HitResponse += HitResponse;
|
|
}
|
|
|
|
LifeUpdated?.Invoke(1);
|
|
BoostUpdated?.Invoke(1);
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
foreach (HitDetection td in _tackleDetectors)
|
|
{
|
|
td.TackledResponse -= TackledResponse;
|
|
td.TacklingResponse -= TacklingResponse;
|
|
td.HitResponse -= HitResponse;
|
|
}
|
|
BoostUpdated = null;
|
|
LifeUpdated = null;
|
|
}
|
|
|
|
// Update is called once per frame
|
|
void FixedUpdate()
|
|
{
|
|
newZone = _forceManager.GetZoneOfInstance(InstanceID);
|
|
// TODO: This could be more elegant maybe?
|
|
if (MatchManager.G.matchState != MatchState.Match || State.IsFrozen)
|
|
{
|
|
_body.constraints = RigidbodyConstraints.FreezeAll;
|
|
UpdateSounds();
|
|
UpdateFireWeapon(equippedWeapon);
|
|
State.Zone = newZone;
|
|
return;
|
|
}
|
|
_body.constraints = RigidbodyConstraints.None;
|
|
UpdateSounds();
|
|
if (State.Zone != newZone)
|
|
{
|
|
State.Zone = newZone;
|
|
}
|
|
UpdateMovement();
|
|
BoostStateUpdate(Time.deltaTime);
|
|
UpdateTackleResponse(_isCriticalTackle);
|
|
UpdateFireWeapon(equippedWeapon);
|
|
}
|
|
|
|
void UpdateFireWeapon(WeaponEffect weapon)
|
|
{
|
|
// Stop firing
|
|
if (State.IsFiring && Input.shootInput < 1 ||
|
|
State.IsFiring && MatchManager.G.matchState != MatchState.Match)
|
|
{
|
|
State.IsFiring = false;
|
|
_fireController.Stop();
|
|
}
|
|
|
|
if (weapon == WeaponEffect.None)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_fireController.SelectedWeaponEffect != weapon)
|
|
{
|
|
_fireController.SelectedWeaponEffect = weapon;
|
|
}
|
|
|
|
if (!State.IsFiring && Input.shootInput == 1)
|
|
{
|
|
State.IsFiring = true;
|
|
_fireController.Fire();
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Movement logic and simulation of the ship.
|
|
/// </summary>
|
|
void UpdateMovement()
|
|
{
|
|
|
|
// Player rotation is always possible and same speed
|
|
|
|
float current_angle = transform.localEulerAngles.z;
|
|
Vector2 radial = -Input.radialInput;
|
|
float goal_angle = Vector2.SignedAngle(Vector2.up, radial) + 180;
|
|
float inputThrust = 0;
|
|
if (radial.magnitude > 0.05)
|
|
{
|
|
float angle_difference = ((goal_angle - current_angle + 180) % 360) - 180;
|
|
angle_difference = angle_difference < -180 ? angle_difference + 360 : angle_difference;
|
|
|
|
float sign = math.sign(angle_difference);
|
|
float rotation = Mathf.Min(math.abs(angle_difference), Props.SteerVelocity * radial.magnitude * Time.deltaTime);
|
|
|
|
transform.Rotate(0, 0, sign * rotation);
|
|
}
|
|
else
|
|
{
|
|
transform.Rotate(0, 0, Input.steerInput * -Props.SteerVelocity * Time.deltaTime);
|
|
}
|
|
inputThrust = Input.thrustInput > inputThrust ? Input.thrustInput : inputThrust;
|
|
|
|
|
|
// Get and apply the current Gravity
|
|
Transform gravitySource = _forceManager.GetGravitySourceForInstance(InstanceID);
|
|
State.CurrentGravity = _forceManager.GetGravityForInstance(InstanceID)(gravitySource, transform) * Props.GravitStrength;
|
|
_body.AddForce(State.CurrentGravity, ForceMode.Acceleration);
|
|
|
|
float stunFactor = _isCriticalTackle ? Props.StunLooseControlFactor : 1f;
|
|
|
|
float thrust = IsBoosting() ? 1f : inputThrust;
|
|
Vector3 acceleration = Props.ThrustAcceleration * thrust * Time.deltaTime
|
|
* transform.up * stunFactor;
|
|
|
|
Vector3 currentVelocity = _body.velocity;
|
|
|
|
Vector3 boostedAcceleration = BoostAcceleration(acceleration, State.CurrentGravity);
|
|
|
|
if (!_isCriticalTackle)
|
|
{
|
|
// Add drag
|
|
if (State.Zone == Zone.NimbleZone)
|
|
{
|
|
Vector3 dragDecceleration = DragDecceleration(currentVelocity, State.Zone);
|
|
_body.AddForce(dragDecceleration, ForceMode.Acceleration);
|
|
|
|
if (!_isTackled)
|
|
{
|
|
// Add anti drift acceleration
|
|
Vector3 driftDampeningAcceleration =
|
|
DriftDampeningAcceleration(currentVelocity, State.Zone);
|
|
_body.AddForce(driftDampeningAcceleration, ForceMode.Acceleration);
|
|
}
|
|
}
|
|
|
|
if (currentVelocity.magnitude <= Props.NormalMaxVelocity || IsBoosting()
|
|
|| State.Zone != Zone.NimbleZone)
|
|
{
|
|
_body.AddForce(boostedAcceleration, ForceMode.Acceleration);
|
|
|
|
}
|
|
if (currentVelocity.magnitude >= Props.AbsolutMaxVelocity && State.Zone == Zone.NimbleZone)
|
|
{
|
|
_body.velocity = _body.velocity.normalized * Props.AbsolutMaxVelocity;
|
|
}
|
|
}
|
|
|
|
// Default torque drag
|
|
_body.AddRelativeTorque(_body.angularVelocity * -Props.TorqueDrag, ForceMode.Acceleration);
|
|
|
|
Debug.DrawRay(transform.position, transform.up * (currentVelocity.magnitude + 3) * 0.5f,
|
|
Color.black);
|
|
|
|
// Fix the ship to the virtual 2D plane of the game
|
|
transform.localEulerAngles = new Vector3(0, 0, transform.localEulerAngles.z);
|
|
_body.transform.localPosition = new Vector3(_body.transform.localPosition.x,
|
|
_body.transform.localPosition.y, 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates a vector to mitigate the ship drifting when it's changing direction.
|
|
/// </summary>
|
|
/// <param name="currentVelocity">Current velocity of the ship</param>
|
|
/// <param name="zone">Zone which the ship is in</param>
|
|
/// <returns></returns>
|
|
Vector3 DriftDampeningAcceleration(Vector3 currentVelocity, Zone zone)
|
|
{
|
|
Vector3 antiDriftVelocity;
|
|
float antiDriftFactor;
|
|
// Cancel out inertia/drifting
|
|
Vector3 up = transform.up;
|
|
Vector3 driftVelocity = currentVelocity - Vector3.Project(currentVelocity, up);
|
|
if (driftVelocity.magnitude < 0.1)
|
|
{
|
|
return Vector3.zero;
|
|
}
|
|
|
|
antiDriftVelocity = Vector3.Reflect(-driftVelocity, up) - driftVelocity;
|
|
antiDriftFactor = Mathf.InverseLerp(Props.AbsolutMaxVelocity, Props.NormalMaxVelocity,
|
|
currentVelocity.magnitude);
|
|
|
|
antiDriftFactor = Mathf.Max(antiDriftFactor, Props.MinAntiDriftFactor);
|
|
|
|
Debug.DrawRay(transform.position, currentVelocity.normalized * currentVelocity.magnitude * 2,
|
|
Color.cyan);
|
|
Debug.DrawRay(transform.position, driftVelocity.normalized * 5, Color.red);
|
|
Debug.DrawRay(transform.position, antiDriftVelocity.normalized * 5, Color.green);
|
|
|
|
return antiDriftVelocity * Props.AntiDriftAmount * antiDriftFactor;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates drag on the ship depending on it's velocity and inhabited zone.
|
|
/// </summary>
|
|
/// <param name="currentVelocity">Velocity of the ship</param>
|
|
/// <param name="zone">Zone which the ship is in</param>
|
|
/// <returns></returns>
|
|
Vector3 DragDecceleration(Vector3 currentVelocity, Zone zone)
|
|
{
|
|
Vector3 drag = new Vector3();
|
|
float minDragFactor = Mathf.InverseLerp(Props.AbsolutMaxVelocity, Props.NormalMaxVelocity,
|
|
currentVelocity.magnitude);
|
|
|
|
float normalDragFactor = Mathf.InverseLerp(Props.NormalMaxVelocity, 0,
|
|
currentVelocity.magnitude);
|
|
|
|
if (!IsBoosting() && zone == Zone.NimbleZone)
|
|
{
|
|
drag -= currentVelocity.normalized * Props.NormalDrag;
|
|
}
|
|
if (currentVelocity.magnitude >= Props.NormalMaxVelocity && zone == Zone.NimbleZone)
|
|
{
|
|
drag -= currentVelocity.normalized * Props.MaximumDrag;
|
|
}
|
|
return drag;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the boost input pressed and boosting possible?
|
|
/// </summary>
|
|
/// <returns>Boosting state</returns>
|
|
bool IsBoosting()
|
|
{
|
|
return Input.boostInput > 0 && _canBoost;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies boost to an acceleration vector.
|
|
/// This includes increasing acceleration and mitigating
|
|
/// the gravity.
|
|
/// </summary>
|
|
/// <param name="acceleration">Current acceleration vector</param>
|
|
/// <param name="currentGravity">Gravity vector which is in force</param>
|
|
/// <returns></returns>
|
|
Vector3 BoostAcceleration(Vector3 acceleration, Vector3 currentGravity)
|
|
{
|
|
if (IsBoosting())
|
|
{
|
|
acceleration *= Props.BoostMagnitude;
|
|
acceleration -= currentGravity * Props.BoostAntiGravityFactor;
|
|
}
|
|
return acceleration;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logic which depletes boost capacity when boost conditions are met.
|
|
/// </summary>
|
|
/// <param name="deltaTime">Time delta of the current frame</param>
|
|
void BoostStateUpdate(float deltaTime)
|
|
{
|
|
BoostUpdated?.Invoke(State.BoostCapacity / Props.MaxBoostCapacity);
|
|
if (IsBoosting())
|
|
{
|
|
State.BoostCapacity -= deltaTime;
|
|
}
|
|
if (_canBoost && State.Zone == Zone.OutsideZone)
|
|
{
|
|
State.BoostCapacity -= deltaTime * Props.OutsideBoostRate;
|
|
}
|
|
if (State.BoostCapacity <= 0)
|
|
{
|
|
_canBoost = false;
|
|
}
|
|
|
|
if ((Input.boostInput <= 0 || !_canBoost)
|
|
&& State.Zone == Zone.NimbleZone
|
|
&& State.BoostCapacity <= Props.MaxBoostCapacity)
|
|
{
|
|
State.BoostCapacity += deltaTime;
|
|
}
|
|
// When your boost capacity is still critical, you can't start boosting immediately again.
|
|
// TODO: This is not tested well enough with players.
|
|
if (_canBoost == false && State.BoostCapacity >= Props.MinBoostCapacity)
|
|
{
|
|
_canBoost = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logic which sets the tackled member variables and
|
|
/// updates them over time.
|
|
/// State logic depends on these variables and is responsible
|
|
/// for certain tackle behavior.
|
|
/// </summary>
|
|
/// <param name="gotTackled">Use true to process a tackle hit</param>
|
|
void UpdateTackleResponse(bool gotTackled = false)
|
|
{
|
|
if (gotTackled && !_isTackled)
|
|
{
|
|
_isTackled = true;
|
|
_tackledTime = _isCriticalTackle ? Props.TackledCriticalStunTime :
|
|
Props.TackledBodyStunTime;
|
|
return;
|
|
}
|
|
_tackledTime -= Time.deltaTime;
|
|
if (_tackledTime <= 0)
|
|
{
|
|
_isTackled = false;
|
|
_isCriticalTackle = false;
|
|
_tackledTime = 0;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Response logic if the ship is tackling an opponend.
|
|
/// </summary>
|
|
void TacklingResponse()
|
|
{
|
|
if (IgnoreTackles())
|
|
return;
|
|
Log.Debug($"{Props.ShipName} is tackling.");
|
|
}
|
|
|
|
bool IgnoreTackles()
|
|
{
|
|
if (Time.time < _lastTackleTime + Props.TackledGraceTime)
|
|
{
|
|
return true;
|
|
}
|
|
_lastTackleTime = Time.time;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by the collision regions of the ship being tackled by an opponent.
|
|
/// Adds resulting forces to the ship and intiates the tackle response.
|
|
/// </summary>
|
|
/// <param name="tackleKind">Kind of the tackle. Depends on collision region.</param>
|
|
/// <param name="collider">Object which has collided with the collision region.</param>
|
|
void TackledResponse(TackleKind tackleKind, Collider collider)
|
|
{
|
|
if (IgnoreTackles())
|
|
return;
|
|
float damage = 0;
|
|
float tacklePowerFactor = Props.CriticalTacklePowerFactor;
|
|
if (tackleKind == TackleKind.IncomingCritical)
|
|
{
|
|
_isCriticalTackle = true;
|
|
damage = 450;
|
|
Log.Debug($"{Props.ShipName} has been tackled critically.");
|
|
}
|
|
else if (tackleKind == TackleKind.IncomingNormal)
|
|
{
|
|
_isCriticalTackle = false;
|
|
damage = 100;
|
|
tacklePowerFactor = Props.NormalTacklePowerFactor;
|
|
Log.Debug($"{Props.ShipName} has been tackled.");
|
|
}
|
|
Vector3 colliderVelocity = collider.attachedRigidbody.velocity - _body.velocity;
|
|
|
|
Vector3 force = colliderVelocity * tacklePowerFactor;
|
|
Vector3 resultForce = force / Math.Max(force.magnitude / 4000, 1);
|
|
|
|
resultForce = resultForce / Math.Max(0.001f, Math.Min(resultForce.magnitude / 500, 1));
|
|
Log.Debug(resultForce.magnitude);
|
|
|
|
_body.AddForce(resultForce,
|
|
ForceMode.Acceleration);
|
|
|
|
InflictDamage(damage);
|
|
DamageParticleEffect.SpawnDamageNumber((int)damage, colliderVelocity / 2);
|
|
UpdateTackleResponse(true);
|
|
}
|
|
|
|
public void HitResponse(HitKind hitKind, ProjectileDamage damage)
|
|
{
|
|
if (Time.time < _lastHitTime + _minHitDelay)
|
|
{
|
|
return;
|
|
}
|
|
_lastHitTime = Time.time;
|
|
InflictDamage(damage.DamageValue);
|
|
Log.Info("particle spawned");
|
|
DamageParticleEffect.SpawnDamageNumber((int)damage.DamageValue, damage.ImpactDirection);
|
|
_body.AddForce(damage.ImpactDirection * damage.ImpactMagnitude, ForceMode.Impulse);
|
|
if ((this as IDamageable).IsKilled())
|
|
{
|
|
MatchManager.G.UpdateMatchCondition(new MatchConditionUpdate
|
|
{
|
|
Condition = WinCondition.Lives,
|
|
Ship = this,
|
|
Count = -1
|
|
});
|
|
}
|
|
}
|
|
|
|
void UpdateSounds()
|
|
{
|
|
if (MatchManager.G.matchState != MatchState.Match || State.IsFrozen)
|
|
{
|
|
|
|
if (newZone != State.Zone
|
|
&& newZone == Zone.NimbleZone)
|
|
{
|
|
AudioManager.G.BroadcastAudioEffect(AudioEffects.LowPass, transform, false);
|
|
}
|
|
sounds[Thruster].StopAudio();
|
|
GravityEffect.Clear();
|
|
GravityEffect.Stop();
|
|
return;
|
|
}
|
|
float velocityFactor = math.smoothstep(0, Props.AbsolutMaxVelocity, _body.velocity.magnitude);
|
|
if (math.abs(Input.thrustInput) > 0 || IsBoosting())
|
|
{
|
|
sounds[Thruster].PlayAudio(true);
|
|
|
|
sounds[Thruster].ChangePitch(velocityFactor);
|
|
if (!JetFlameEffect.isPlaying)
|
|
JetFlameEffect.Play();
|
|
}
|
|
else
|
|
{
|
|
sounds[Thruster].FadeOutAudio(0.3f);
|
|
JetFlameEffect.Stop();
|
|
}
|
|
if (IsBoosting())
|
|
{
|
|
if (!BoostEffect.isPlaying)
|
|
BoostEffect.Play();
|
|
if (!SmokeTrailEffect.isPlaying)
|
|
SmokeTrailEffect.Play();
|
|
if (JetFlameEffect.isPlaying)
|
|
JetFlameEffect.transform.localScale = new Vector3(1.3f, 2, 1);
|
|
sounds[Booster].PlayAudio(false, 0.1f, true);
|
|
}
|
|
else
|
|
{
|
|
sounds[Booster].ResetOneShot();
|
|
SmokeTrailEffect.Stop();
|
|
JetFlameEffect.transform.localScale = new Vector3(1.3f, 1, 1);
|
|
}
|
|
if (_isTackled && !_isCriticalTackle)
|
|
{
|
|
sounds[Tackling].PlayAudio(false, 0, true);
|
|
CameraOperator.ShakeCam(0.2f);
|
|
}
|
|
if (_isCriticalTackle)
|
|
{
|
|
sounds[TacklingCritical].PlayAudio(false, 0, true);
|
|
CameraOperator.ShakeCam(0.4f);
|
|
}
|
|
if (!_isTackled)
|
|
{
|
|
sounds[TacklingCritical].ResetOneShot();
|
|
sounds[Tackling].ResetOneShot();
|
|
}
|
|
if (newZone != State.Zone
|
|
&& State.Zone != Zone.UninitializedZone
|
|
&& newZone != Zone.UninitializedZone)
|
|
{
|
|
if (newZone != Zone.NimbleZone)
|
|
{
|
|
sounds[LeaveZone].ChangePitch(velocityFactor);
|
|
sounds[LeaveZone].PlayAudio(false);
|
|
AudioManager.G.BroadcastAudioEffect(AudioEffects.LowPass, transform, true);
|
|
}
|
|
else
|
|
{
|
|
sounds[EnterZone].ChangePitch(velocityFactor);
|
|
sounds[EnterZone].PlayAudio(false);
|
|
AudioManager.G.BroadcastAudioEffect(AudioEffects.LowPass, transform, false);
|
|
}
|
|
}
|
|
|
|
if (GravityEffect == null)
|
|
{
|
|
return;
|
|
}
|
|
if (!GravityEffect.isPlaying && State.CurrentGravity != Vector3.zero)
|
|
{
|
|
GravityEffect.Play();
|
|
}
|
|
else if (State.CurrentGravity == Vector3.zero)
|
|
{
|
|
GravityEffect.Stop();
|
|
}
|
|
if (GravityEffect.isPlaying)
|
|
{
|
|
float gravityAngle =
|
|
Vector3.SignedAngle(transform.parent.up, State.CurrentGravity, transform.forward);
|
|
GravityEffect.gameObject.transform.localEulerAngles =
|
|
new Vector3(0, 0, gravityAngle - transform.localEulerAngles.z);
|
|
|
|
}
|
|
}
|
|
|
|
public void EquipWeapon(WeaponEffect weapon)
|
|
{
|
|
if (equippedWeapon != WeaponEffect.None)
|
|
{
|
|
_fireController.Stop();
|
|
}
|
|
equippedWeapon = weapon;
|
|
}
|
|
|
|
public float CurrentHealth()
|
|
{
|
|
return State.RemainingHealth;
|
|
}
|
|
|
|
public float MaximumHealth()
|
|
{
|
|
return Props.MaximumHealth;
|
|
}
|
|
|
|
public void SetHealth(float totalValue)
|
|
{
|
|
State.RemainingHealth = totalValue;
|
|
LifeUpdated?.Invoke(CurrentHealth() / MaximumHealth());
|
|
}
|
|
|
|
public void InflictDamage(float damageValue)
|
|
{
|
|
SetHealth(CurrentHealth() - damageValue);
|
|
}
|
|
|
|
public void ReplenishHealth(float healValue)
|
|
{
|
|
SetHealth(CurrentHealth() + healValue);
|
|
}
|
|
|
|
|
|
}
|