2026-10-09 20:19:23 +02:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using UniRx;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
using UnityEngine;
|
Fire control Cut 3: hit authority moves to FireControl
Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the
raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and
Lightning) and replaces it with a single owner in the same cut: FireControl
now decides whether a shot hits, Zone owns the pending-shot lifecycle, and
Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the
result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies
nothing.
FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor
* pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the
weapon's payload and the shooter's resolved targeting stats at the instant the
trigger is pulled (Q6), computes the predicted intercept through the one
shared PredictedIntercept function (CombatState now calls the same function
instead of duplicating first_order_intercept), and queues a PendingShot.
FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon --
deviation from the predicted intercept, measured once, live, at that instant
-- and applies it at ArrivalTime. A shot whose flight is shorter than the
horizon commits and resolves in the same tick it fires. Rolls draw from the
shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc
or otherwise gated shot draws nothing.
Player fire is arc-gated through the same predicate AI and turret fire use
(Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every
shooter's trigger passes through), closing Q2.
Adds GameplaySettings.CommitHorizon, SchematicCellSize and
AgentMinHitProbability (first guesses; the headless suite is the tuning
harness, operator play smoke is the arbiter).
FORK reported, not worked around silently: HullCollider.SendHit/SendSplash
cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs,
ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them
from their own live Physics queries -- that's Cut 4's migration. They fire
into the void today: nothing subscribes to Hit/Splash any more for the four
weapon kinds this cut owns.
tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the
gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its
evasion window, short-flight degradation, the frozen-payload snapshot, aimed
hits, the item-then-hull damage order, the planar penetration march and the
shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are
updated for the new AI gate and OnFire(int) signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
|
|
|
|
using static CultMath.math;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
2026-10-09 20:19:23 +02:00
|
|
|
|
// 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
|
2026-10-09 20:35:37 +02:00
|
|
|
|
// burst. A round that missed flies on to the MaxRange point RoundAt clamps at, and is done there. The barrel is only
|
2026-10-09 20:19:23 +02:00
|
|
|
|
// 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).
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public class Projectile : MonoBehaviour
|
|
|
|
|
|
{
|
|
|
|
|
|
public TrailRenderer Trail;
|
|
|
|
|
|
public Prototype HitEffect;
|
Fire control Cut 3: hit authority moves to FireControl
Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the
raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and
Lightning) and replaces it with a single owner in the same cut: FireControl
now decides whether a shot hits, Zone owns the pending-shot lifecycle, and
Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the
result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies
nothing.
FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor
* pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the
weapon's payload and the shooter's resolved targeting stats at the instant the
trigger is pulled (Q6), computes the predicted intercept through the one
shared PredictedIntercept function (CombatState now calls the same function
instead of duplicating first_order_intercept), and queues a PendingShot.
FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon --
deviation from the predicted intercept, measured once, live, at that instant
-- and applies it at ArrivalTime. A shot whose flight is shorter than the
horizon commits and resolves in the same tick it fires. Rolls draw from the
shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc
or otherwise gated shot draws nothing.
Player fire is arc-gated through the same predicate AI and turret fire use
(Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every
shooter's trigger passes through), closing Q2.
Adds GameplaySettings.CommitHorizon, SchematicCellSize and
AgentMinHitProbability (first guesses; the headless suite is the tuning
harness, operator play smoke is the arbiter).
FORK reported, not worked around silently: HullCollider.SendHit/SendSplash
cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs,
ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them
from their own live Physics queries -- that's Cut 4's migration. They fire
into the void today: nothing subscribes to Hit/Splash any more for the four
weapon kinds this cut owns.
tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the
gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its
evasion window, short-flight degradation, the frozen-payload snapshot, aimed
hits, the item-then-hull damage order, the planar penetration march and the
shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are
updated for the new AI gate and OnFire(int) signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
|
|
|
|
|
2026-10-09 20:19:23 +02:00
|
|
|
|
// Sim seconds over which the barrel's offset from the sim line decays to nothing.
|
|
|
|
|
|
public float BlendTime = .1f;
|
Fire control Cut 3: hit authority moves to FireControl
Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the
raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and
Lightning) and replaces it with a single owner in the same cut: FireControl
now decides whether a shot hits, Zone owns the pending-shot lifecycle, and
Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the
result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies
nothing.
FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor
* pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the
weapon's payload and the shooter's resolved targeting stats at the instant the
trigger is pulled (Q6), computes the predicted intercept through the one
shared PredictedIntercept function (CombatState now calls the same function
instead of duplicating first_order_intercept), and queues a PendingShot.
FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon --
deviation from the predicted intercept, measured once, live, at that instant
-- and applies it at ArrivalTime. A shot whose flight is shorter than the
horizon commits and resolves in the same tick it fires. Rolls draw from the
shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc
or otherwise gated shot draws nothing.
Player fire is arc-gated through the same predicate AI and turret fire use
(Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every
shooter's trigger passes through), closing Q2.
Adds GameplaySettings.CommitHorizon, SchematicCellSize and
AgentMinHitProbability (first guesses; the headless suite is the tuning
harness, operator play smoke is the arbiter).
FORK reported, not worked around silently: HullCollider.SendHit/SendSplash
cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs,
ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them
from their own live Physics queries -- that's Cut 4's migration. They fire
into the void today: nothing subscribes to Hit/Splash any more for the four
weapon kinds this cut owns.
tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the
gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its
evasion window, short-flight degradation, the frozen-payload snapshot, aimed
hits, the item-then-hull damage order, the planar penetration march and the
shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are
updated for the new AI gate and OnFire(int) signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
|
|
|
|
|
2026-10-09 20:19:23 +02:00
|
|
|
|
private PendingShot _shot;
|
|
|
|
|
|
private Zone _zone;
|
|
|
|
|
|
private Vector3 _barrelOffset;
|
|
|
|
|
|
private bool _alive;
|
|
|
|
|
|
private IDisposable _binding;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
2026-10-09 20:19:23 +02:00
|
|
|
|
// 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)
|
2026-08-10 14:57:50 +01:00
|
|
|
|
{
|
2026-10-09 20:19:23 +02:00
|
|
|
|
_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);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
_alive = true;
|
2026-10-09 20:19:23 +02:00
|
|
|
|
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;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Update()
|
|
|
|
|
|
{
|
2026-10-09 20:19:23 +02:00
|
|
|
|
if (!_alive) return;
|
|
|
|
|
|
var time = _zone.Time + ActionGameManager.Clock.Lead;
|
|
|
|
|
|
transform.position = At(FireControl.RoundAt(_shot, time), time);
|
2026-10-09 20:35:37 +02:00
|
|
|
|
var end = _shot.Speed > .01f ? _shot.FireTime + _shot.MaxRange / _shot.Speed : _shot.FireTime;
|
2026-10-09 20:19:23 +02:00
|
|
|
|
if (time >= end) Finish();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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<Transform>().position = transform.position;
|
|
|
|
|
|
Finish();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void Finish()
|
|
|
|
|
|
{
|
|
|
|
|
|
_alive = false;
|
|
|
|
|
|
_binding?.Dispose();
|
|
|
|
|
|
_binding = null;
|
|
|
|
|
|
StartCoroutine(Fade());
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-09 20:19:23 +02:00
|
|
|
|
void OnDisable()
|
2026-08-10 14:57:50 +01:00
|
|
|
|
{
|
|
|
|
|
|
_alive = false;
|
2026-10-09 20:19:23 +02:00
|
|
|
|
_binding?.Dispose();
|
|
|
|
|
|
_binding = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IEnumerator Fade()
|
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
var startTime = Time.time;
|
|
|
|
|
|
var lifetime = Trail.time;
|
|
|
|
|
|
while (Time.time - startTime < lifetime)
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
GetComponent<Prototype>().ReturnToPool();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|