Add live fire control diagnostics
This commit is contained in:
parent
6056327935
commit
72c0109cf1
2 changed files with 132 additions and 20 deletions
|
|
@ -148,6 +148,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
public float HitMarkerDuration;
|
||||
public SchematicDisplay SchematicDisplay;
|
||||
public SchematicDisplay TargetSchematicDisplay;
|
||||
public TextMeshProUGUI DebugInfoText;
|
||||
|
||||
[Header("Target Indicator")]
|
||||
public PlaceUIElementWorldspace TargetIndicator;
|
||||
|
|
@ -183,6 +184,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
private Dictionary<Entity, VisibleTargetIndicator> _visibleFriendlyIndicators = new Dictionary<Entity, VisibleTargetIndicator>();
|
||||
private List<IDisposable> _shipSubscriptions = new List<IDisposable>();
|
||||
private List<IDisposable> _targetSubscriptions = new List<IDisposable>();
|
||||
private readonly Dictionary<EquippedItem, ShotOutcome> _debugLastShots = new Dictionary<EquippedItem, ShotOutcome>();
|
||||
private float _severeHeatstrokePhase;
|
||||
private bool _uiHidden;
|
||||
private bool _menuShown;
|
||||
|
|
@ -981,6 +983,8 @@ public class ActionGameManager : MonoBehaviour
|
|||
|
||||
foreach(var subscription in _shipSubscriptions) subscription.Dispose();
|
||||
_shipSubscriptions.Clear();
|
||||
_debugLastShots.Clear();
|
||||
if (DebugInfoText != null) DebugInfoText.text = "";
|
||||
}
|
||||
|
||||
private void BindToEntity(Entity entity)
|
||||
|
|
@ -1009,6 +1013,10 @@ public class ActionGameManager : MonoBehaviour
|
|||
GameplayUI.gameObject.SetActive(true);
|
||||
ShipPanel.Display(CurrentEntity, true);
|
||||
SchematicDisplay.ShowShip(CurrentEntity);
|
||||
|
||||
_shipSubscriptions.Add(CurrentEntity.Zone.ShotResolved
|
||||
.Where(outcome => outcome.Source == CurrentEntity)
|
||||
.Subscribe(outcome => _debugLastShots[outcome.Weapon] = outcome));
|
||||
|
||||
FollowCamera.LookAt = ZoneRenderer.EntityInstances[CurrentEntity].LookAtPoint;
|
||||
FollowCamera.Follow = ZoneRenderer.EntityInstances[CurrentEntity].transform;
|
||||
|
|
@ -1256,6 +1264,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
}
|
||||
|
||||
var target = CurrentEntity.Target.Value;
|
||||
UpdateFireControlDebug(target);
|
||||
if (target != null)
|
||||
{
|
||||
var threshold = Settings.GameplaySettings.TargetDetectionInfoThreshold;
|
||||
|
|
@ -1273,6 +1282,62 @@ public class ActionGameManager : MonoBehaviour
|
|||
}
|
||||
}
|
||||
|
||||
private void UpdateFireControlDebug(Entity target)
|
||||
{
|
||||
if (DebugInfoText == null) return;
|
||||
|
||||
EquippedItem selectedItem = null;
|
||||
Weapon selectedWeapon = null;
|
||||
foreach (var item in CurrentEntity.Equipment)
|
||||
{
|
||||
var weapon = item.Behaviors.OfType<Weapon>().FirstOrDefault(x => !(x is LockWeapon));
|
||||
if (weapon == null) continue;
|
||||
selectedItem = item;
|
||||
selectedWeapon = weapon;
|
||||
break;
|
||||
}
|
||||
|
||||
if (selectedWeapon == null)
|
||||
{
|
||||
DebugInfoText.text = "FIRE CONTROL\nNo non-lock weapon";
|
||||
return;
|
||||
}
|
||||
|
||||
var d = FireControl.Inspect(selectedWeapon, CurrentEntity, target);
|
||||
var pendingLine = "pending: none";
|
||||
for (var i = CurrentEntity.Zone.PendingShots.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var shot = CurrentEntity.Zone.PendingShots[i];
|
||||
if (shot.Source != CurrentEntity || shot.Weapon != selectedItem) continue;
|
||||
if (shot.Committed)
|
||||
pendingLine = $"shot {shot.ShotId}: committed {(shot.Outcome.Hit ? "HIT" : "MISS")}";
|
||||
else
|
||||
{
|
||||
var pDeviation = FireControl.DeviationProbability(shot, CurrentEntity.Zone.Time, out var deviation);
|
||||
pendingLine = $"shot {shot.ShotId}: dev {deviation:F1}/{shot.Tracking:F1} x{pDeviation:F3} estimate {shot.PBase * pDeviation:P1}";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var lastLine = _debugLastShots.TryGetValue(selectedItem, out var lastShot)
|
||||
? $"last {lastShot.ShotId}: {(lastShot.Hit ? "HIT" : "MISS")} cell {lastShot.Cell.x},{lastShot.Cell.y}"
|
||||
: "last: none";
|
||||
var gates = target == null
|
||||
? "target: none"
|
||||
: $"gates vis {d.Visible} range {d.InRange} arc {d.InArc} lock {d.Locked}";
|
||||
|
||||
DebugInfoText.text =
|
||||
$"FIRE CONTROL - {selectedItem.Data.Name}\n" +
|
||||
$"{gates}\n" +
|
||||
$"range {d.Range:F0} [{d.MinRange:F0}..{d.MaxRange:F0}]\n" +
|
||||
$"info {d.Info:F3}/{d.InfoDemandCeiling:F3} sensor {d.PSensor:F3}\n" +
|
||||
$"accuracy {d.Accuracy:F3} spread {d.PSpread:F3} hull {d.POnHull:F3}\n" +
|
||||
$"precision {d.Precision:F3} tracking {d.Tracking:F1}\n" +
|
||||
$"base {d.PBase:P1}\n" +
|
||||
$"{pendingLine}\n" +
|
||||
lastLine;
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
UpdateTargetIndicators();
|
||||
|
|
|
|||
|
|
@ -143,16 +143,33 @@ public static class FireControl
|
|||
// a tick apart may differ; nothing here or anywhere else caches one.
|
||||
public static float HitProbability(Weapon weapon, Entity source, Entity target)
|
||||
{
|
||||
if (target == null) return 0f;
|
||||
if (!source.VisibleEntities.Contains(target)) return 0f;
|
||||
return Inspect(weapon, source, target).PBase;
|
||||
}
|
||||
|
||||
// Presentation-only inspection of the exact factors HitProbability owns. Keeping this calculation here
|
||||
// prevents the debug HUD from growing a second, subtly different fire-control model.
|
||||
public static FireControlDiagnostic Inspect(Weapon weapon, Entity source, Entity target)
|
||||
{
|
||||
var settings = source.ItemManager.GameplaySettings;
|
||||
var diagnostic = new FireControlDiagnostic
|
||||
{
|
||||
HasTarget = target != null,
|
||||
Accuracy = Accuracy(source),
|
||||
Resolution = Resolution(source),
|
||||
Precision = Precision(source),
|
||||
Tracking = Tracking(source),
|
||||
MinRange = weapon.MinRange,
|
||||
MaxRange = weapon.Range
|
||||
};
|
||||
if (target == null) return diagnostic;
|
||||
|
||||
var toTarget = target.Position - source.Position;
|
||||
var range = length(toTarget);
|
||||
if (range < weapon.MinRange || range > weapon.Range) return 0f;
|
||||
if (weapon is LockWeapon lockWeapon && !lockWeapon.IsLocked) return 0f;
|
||||
if (!InArc(weapon.Item, toTarget)) return 0f;
|
||||
diagnostic.Range = length(toTarget);
|
||||
diagnostic.Visible = source.VisibleEntities.Contains(target);
|
||||
diagnostic.InRange = diagnostic.Range >= weapon.MinRange && diagnostic.Range <= weapon.Range;
|
||||
diagnostic.Locked = !(weapon is LockWeapon lockWeapon) || lockWeapon.IsLocked;
|
||||
diagnostic.InArc = InArc(weapon.Item, toTarget);
|
||||
|
||||
var settings = source.ItemManager.GameplaySettings;
|
||||
var info = source.EntityInfoGathered.TryGetValue(target, out var gathered) ? gathered : 0f;
|
||||
// Cut 6c, 6c.1 (operator ruling 2026-09-19): Resolution stays a benefit -- higher is better -- so the
|
||||
// formula takes its reciprocal to derive the actual info ceiling instead of reading Resolution as
|
||||
|
|
@ -160,20 +177,20 @@ public static class FireControl
|
|||
// complete information, the floor by construction. A higher Resolution pulls the ceiling down toward
|
||||
// the detection threshold, needing less info before sensor state stops limiting hits. The max guards
|
||||
// a zero or negative authored Resolution; no authored value should ever reach it.
|
||||
var demandCeiling = settings.TargetDetectionInfoThreshold +
|
||||
(1f - settings.TargetDetectionInfoThreshold) / max(Resolution(source), 1e-3f);
|
||||
var pSensor = saturate(unlerp(settings.TargetDetectionInfoThreshold, demandCeiling, info));
|
||||
diagnostic.Info = info;
|
||||
diagnostic.InfoDemandCeiling = settings.TargetDetectionInfoThreshold +
|
||||
(1f - settings.TargetDetectionInfoThreshold) / max(diagnostic.Resolution, 1e-3f);
|
||||
diagnostic.PSensor = saturate(unlerp(settings.TargetDetectionInfoThreshold, diagnostic.InfoDemandCeiling, info));
|
||||
|
||||
var targetHull = source.ItemManager.GetData(target.Hull) as HullData;
|
||||
|
||||
float pSpread;
|
||||
diagnostic.PSpread = 1f;
|
||||
if (weapon.Spread > 0)
|
||||
{
|
||||
var halfExtent = .5f * max(targetHull.Shape.Width, targetHull.Shape.Height) * settings.SchematicCellSize;
|
||||
var angularRadius = degrees(atan(halfExtent / range));
|
||||
pSpread = saturate(angularRadius / (weapon.Spread / 2f));
|
||||
var angularRadius = degrees(atan(halfExtent / diagnostic.Range));
|
||||
diagnostic.PSpread = saturate(angularRadius / (weapon.Spread / 2f));
|
||||
}
|
||||
else pSpread = 1f;
|
||||
|
||||
// Cut 6d (docs/fire-control-cut.md): the dart-throw kernel's other half. pSpread above prices whether
|
||||
// the weapon's own barrel-dispersion cone even reaches the ship's silhouette at this range (weapon
|
||||
|
|
@ -183,9 +200,20 @@ public static class FireControl
|
|||
// range-blind, aim-point-aware). Different inputs, different failure modes; see Cut(0's report for the
|
||||
// double-charging call). Aim point is source.ResolvedTargetItem, exactly what Fire freezes below --
|
||||
// HitProbability and Fire read the same reveal-gated aim point, never two.
|
||||
var pOnHull = HullKernel(targetHull, ResolveAimPoint(target, targetHull, source.ResolvedTargetItem).AimPoint, Precision(source)).POnHull;
|
||||
diagnostic.POnHull = HullKernel(targetHull,
|
||||
ResolveAimPoint(target, targetHull, source.ResolvedTargetItem).AimPoint, diagnostic.Precision).POnHull;
|
||||
|
||||
return Accuracy(source) * pSensor * pSpread * pOnHull;
|
||||
if (diagnostic.Visible && diagnostic.InRange && diagnostic.Locked && diagnostic.InArc)
|
||||
diagnostic.PBase = diagnostic.Accuracy * diagnostic.PSensor * diagnostic.PSpread * diagnostic.POnHull;
|
||||
return diagnostic;
|
||||
}
|
||||
|
||||
public static float DeviationProbability(PendingShot shot, float now, out float deviation)
|
||||
{
|
||||
var elapsed = now - shot.FireTime;
|
||||
var predicted = shot.FireTargetPosition + shot.FireTargetVelocity * elapsed;
|
||||
deviation = shot.Target == null ? 0f : length((shot.Target.Position - predicted).xz);
|
||||
return shot.Target == null ? 1f : saturate(1f - deviation / shot.Tracking);
|
||||
}
|
||||
|
||||
// Cut 3, R1: called once per burst step from InstantWeapon.Execute. Computes flight time, freezes the
|
||||
|
|
@ -329,12 +357,9 @@ public static class FireControl
|
|||
var p = shot.PBase;
|
||||
if (p > 0f && shot.Target != null)
|
||||
{
|
||||
var elapsed = now - shot.FireTime;
|
||||
var predicted = shot.FireTargetPosition + shot.FireTargetVelocity * elapsed;
|
||||
var deviation = length((shot.Target.Position - predicted).xz);
|
||||
var pDeviation = DeviationProbability(shot, now, out _);
|
||||
// Cut 5, 5.1: Tracking is always positive now (Tracking() above never returns 0), so the branch
|
||||
// that used to turn a Tracking-less shooter's forgiveness into a hard <.01f wall is gone outright.
|
||||
var pDeviation = saturate(1f - deviation / shot.Tracking);
|
||||
p *= pDeviation;
|
||||
}
|
||||
|
||||
|
|
@ -570,6 +595,28 @@ public struct PendingShot
|
|||
public ShotOutcome Outcome;
|
||||
}
|
||||
|
||||
public struct FireControlDiagnostic
|
||||
{
|
||||
public bool HasTarget;
|
||||
public bool Visible;
|
||||
public bool InRange;
|
||||
public bool Locked;
|
||||
public bool InArc;
|
||||
public float Range;
|
||||
public float MinRange;
|
||||
public float MaxRange;
|
||||
public float Info;
|
||||
public float InfoDemandCeiling;
|
||||
public float Accuracy;
|
||||
public float Resolution;
|
||||
public float Precision;
|
||||
public float Tracking;
|
||||
public float PSensor;
|
||||
public float PSpread;
|
||||
public float POnHull;
|
||||
public float PBase;
|
||||
}
|
||||
|
||||
// Cut 3 (docs/fire-control-cut.md, 0b table): a commit. Created once, at ArrivalTime - CommitHorizon (or at
|
||||
// fire time when the flight is shorter than the horizon), immutable from then on, published once on
|
||||
// Zone.ShotCommitted and again (unchanged) on Zone.ShotResolved at arrival. Presentations are the only
|
||||
|
|
|
|||
Loading…
Reference in a new issue