diff --git a/Assets/Scripts/Gameplay/Weapons/Projectile.cs b/Assets/Scripts/Gameplay/Weapons/Projectile.cs index 70b591649..4e38ad549 100644 --- a/Assets/Scripts/Gameplay/Weapons/Projectile.cs +++ b/Assets/Scripts/Gameplay/Weapons/Projectile.cs @@ -1,59 +1,87 @@ -using System.Collections; +using System; +using System.Collections; +using UniRx; using UnityEngine; using static CultMath.math; -// Cut 3 (docs/fire-control-cut.md): the raycast, shield branch and SendHit are deleted -- FireControl already -// decided this shot's fate before this object was ever spawned (R8). What is left is pure flight-path -// presentation: fly, and disappear at Range or ShotId's resolution, whichever comes first. -// Cut 6b, 6.2 (12.4(b): Apply's own switch on the frozen Fuse decides this now, not Step): the flak-round -// blast resolves in the simulation, through FireControl.Detonate -- a MonoBehaviour calling it would be -// exactly the authority Cut 3 spent itself deleting, so the blast-distance/blast-radius fields this object -// used to carry for that are gone. This object's job stays fly-and-disappear. +// A ballistic round, drawn on FireControl.RoundAt and deciding nothing. The simulation owns the round's whole flight +// (origin, direction, speed, fire time, arrival, range: the PendingShot copy taken at spawn), so this object stores no +// velocity or position of its own and integrates nothing: each frame it asks RoundAt where the round is at sim time +// plus the clock's Lead (the same draw-ahead ships use), and the simulation's ShotResolved ends a round that hit or +// burst. A round that missed flies on to the FireRange point RoundAt clamps at, and is done there. The barrel is only +// where the round is first seen: its offset from the sim line decays to zero over BlendTime sim seconds. The trail's +// fade after the end stays real time (pure presentation, ruling sim-speed-presentation). public class Projectile : MonoBehaviour { public TrailRenderer Trail; - public float Gravity; - public float Drag = .1f; public Prototype HitEffect; + // Sim seconds over which the barrel's offset from the sim line decays to nothing. + public float BlendTime = .1f; + + private PendingShot _shot; + private Zone _zone; + private Vector3 _barrelOffset; private bool _alive; + private IDisposable _binding; - // Cut 3: FireControl.Fire's ShotId -- this projectile's handle onto Zone.ShotCommitted/ShotResolved. - public int ShotId { get; set; } - public Zone Zone { get; set; } - - public Vector3 StartPosition { get; set; } - public Vector3 Velocity { get; set; } - public Entity SourceEntity { get; set; } - public float Range { get; set; } - - private void OnEnable() + // Takes the frozen shot, the zone whose sim time it is drawn at, and where the barrel is as this round is first seen. + public void Launch(Zone zone, PendingShot shot, Vector3 barrel) { + _zone = zone; + _shot = shot; + var origin = shot.FireOrigin; + _barrelOffset = barrel - new Vector3(origin.x, origin.y, origin.z); + _binding?.Dispose(); + _binding = zone.ShotResolved.Where(o => o.ShotId == shot.ShotId).Subscribe(Resolve); _alive = true; + transform.forward = new Vector3(shot.TravelDirection.x, 0, shot.TravelDirection.y); + transform.position = barrel; + } + + private Vector3 At(CultMath.float2 planar, float time) + { + var blend = BlendTime > 0 ? saturate(1 - (time - _shot.FireTime) / BlendTime) : 0f; + return new Vector3(planar.x, _shot.FireOrigin.y, planar.y) + _barrelOffset * blend; } - // Update is called once per frame void Update() { - if (SourceEntity == null) return; - - if(_alive) - { - var t = transform; - Velocity -= Vector3.up * (Gravity * Time.deltaTime); - Velocity *= max(0, 1 - Drag * Time.deltaTime); - t.forward = Velocity.normalized; - - transform.position += Velocity * Time.deltaTime; - var distanceTraveled = (transform.position - StartPosition).magnitude; - if(distanceTraveled > Range) - StartCoroutine(Kill()); - } + if (!_alive) return; + var time = _zone.Time + ActionGameManager.Clock.Lead; + transform.position = At(FireControl.RoundAt(_shot, time), time); + var end = _shot.Speed > .01f ? _shot.FireTime + _shot.FireRange / _shot.Speed : _shot.FireTime; + if (time >= end) Finish(); } - IEnumerator Kill() + // The simulation's verdict. A round that hit or burst stops where the sim put it, at the outcome's burst point, + // else where it arrives on its line; a miss keeps flying. + private void Resolve(ShotOutcome outcome) + { + if (!_alive || (outcome.Result != ShotResult.Hit && outcome.Result != ShotResult.Burst)) return; + var planar = outcome.HasBurstPoint ? outcome.BurstPoint : FireControl.RoundAt(_shot, _shot.ArrivalTime); + transform.position = At(planar, _shot.ArrivalTime); + if (HitEffect != null) HitEffect.Instantiate().position = transform.position; + Finish(); + } + + private void Finish() { _alive = false; + _binding?.Dispose(); + _binding = null; + StartCoroutine(Fade()); + } + + void OnDisable() + { + _alive = false; + _binding?.Dispose(); + _binding = null; + } + + IEnumerator Fade() + { var startTime = Time.time; var lifetime = Trail.time; while (Time.time - startTime < lifetime) diff --git a/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs b/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs index 56cfdcf80..83eba9ea4 100644 --- a/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs +++ b/Assets/Scripts/Gameplay/Weapons/ProjectileManager.cs @@ -1,32 +1,17 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using Random = UnityEngine.Random; +using UnityEngine; public class ProjectileManager : InstantWeaponEffectManager { public Prototype ProjectilePrototype; - public bool InheritVelocity; + // The round is drawn on the line the simulation flies it (FireControl.RoundAt), so there is nothing to aim, jitter + // or inherit here: spread is priced by FireControl, and the sim's round inherits nothing from its shooter. public override void Fire(InstantWeapon weapon, EquippedItem item, EntityInstance source, EntityInstance target, int shotId) { + var zone = source.Entity.Zone; + if (!zone.TryGetShot(shotId, out var shot)) return; var p = ProjectilePrototype.Instantiate(); - var hp = item.Hardpoint; - var barrel = source.GetBarrel(hp); - var angle = weapon.Spread / 2; - p.ShotId = shotId; - p.SourceEntity = source.Entity; - p.Velocity = Quaternion.Euler( - Random.Range(-angle, angle), - Random.Range(-angle, angle), - Random.Range(-angle, angle)) * - barrel.forward * - weapon.Velocity; - p.StartPosition = p.transform.position = barrel.position + p.Velocity * (Random.value * Time.deltaTime); - if(InheritVelocity) - p.Velocity += new Vector3(source.Entity.Velocity.x, 0, source.Entity.Velocity.y); - p.Range = weapon.Range; - p.Zone = source.Entity.Zone; + p.Launch(zone, shot, source.GetBarrel(item.Hardpoint).position); p.Trail.Clear(); } }