Lock warning: a painted ship owns a typed incoming-lock warning carrying the lock's strength

LockWeapon.Execute paints its target with its own Lock (the one value the outgoing indicator also reads);
Entity publishes the one-tick inbox as IncomingLocks; the HUD shows one incoming indicator on the strongest
warning with the outgoing indicator's curves. Unity HUD code not compiled here; tests not yet run at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-10 19:25:22 +02:00
commit e5cfc0d676
4 changed files with 320 additions and 0 deletions

View file

@ -157,6 +157,8 @@ public class ActionGameManager : MonoBehaviour
public Prototype FriendlyTargetIndicator;
public PlaceUIElementWorldspace ViewDot;
public Prototype LockIndicator;
// Presentation tuning: how far from the ship, toward the locker, the incoming-lock indicator sits.
public float IncomingLockIndicatorDistance = 30;
public PlaceUIElementWorldspace[] Crosshairs;
public GameObject HitMarker;
public float HitMarkerDuration;
@ -195,6 +197,7 @@ public class ActionGameManager : MonoBehaviour
private TextMeshProUGUI _faceAimMark;
private (HardpointData[] hardpoints, Transform[] barrels, PlaceUIElementWorldspace crosshair)[] _articulationGroups;
private (LockWeapon targetLock, PlaceUIElementWorldspace indicator, Rotate spin)[] _lockingIndicators;
private (PlaceUIElementWorldspace indicator, Rotate spin) _incomingLockIndicator;
private Dictionary<Entity, VisibleTargetIndicator> _visibleHostileIndicators = new Dictionary<Entity, VisibleTargetIndicator>();
private Dictionary<Entity, VisibleTargetIndicator> _visibleFriendlyIndicators = new Dictionary<Entity, VisibleTargetIndicator>();
private List<IDisposable> _shipSubscriptions = new List<IDisposable>();
@ -999,6 +1002,8 @@ public class ActionGameManager : MonoBehaviour
if(_lockingIndicators!=null) foreach(var (_, indicator, _) in _lockingIndicators)
indicator.GetComponent<Prototype>().ReturnToPool();
if(_incomingLockIndicator.indicator != null) _incomingLockIndicator.indicator.GetComponent<Prototype>().ReturnToPool();
_incomingLockIndicator = default;
DisablePlayerInput();
Cursor.lockState = CursorLockMode.None;
GameplayUI.gameObject.SetActive(false);
@ -1142,6 +1147,10 @@ public class ActionGameManager : MonoBehaviour
var i = LockIndicator.Instantiate<PlaceUIElementWorldspace>();
return (x, i, i.GetComponent<Rotate>());
}).ToArray();
var incoming = LockIndicator.Instantiate<PlaceUIElementWorldspace>();
incoming.gameObject.SetActive(false);
_incomingLockIndicator = (incoming, incoming.GetComponent<Rotate>());
}
private void UpdatePlayerPanel()
@ -1449,6 +1458,24 @@ public class ActionGameManager : MonoBehaviour
spin.Speed = Settings.GameplaySettings.LockSpinSpeed.Evaluate(targetLock.Lock);
}
}
// The strongest lock on the player's ship, read only from IncomingLocks (never another ship's LockWeapon).
// Same curves as the outgoing indicator, driven by the warning's Strength.
var warnings = CurrentEntity.IncomingLocks;
var strongest = -1;
for (var w = 0; w < warnings.Count; w++)
if (strongest < 0 || warnings[w].Strength > warnings[strongest].Strength) strongest = w;
var (incomingIndicator, incomingSpin) = _incomingLockIndicator;
incomingIndicator.gameObject.SetActive(strongest >= 0);
if (strongest >= 0)
{
var warning = warnings[strongest];
incomingIndicator.Target = CurrentEntity.Position.ToUnity() +
new Vector3(warning.Bearing.x, 0, warning.Bearing.y) * IncomingLockIndicatorDistance;
incomingIndicator.NoiseAmplitude = Settings.GameplaySettings.LockIndicatorNoiseAmplitude * (1 - warning.Strength);
incomingIndicator.NoiseFrequency = Settings.GameplaySettings.LockIndicatorFrequency.Evaluate(warning.Strength);
incomingSpin.Speed = Settings.GameplaySettings.LockSpinSpeed.Evaluate(warning.Strength);
}
}
}

View file

@ -102,6 +102,7 @@ public class LockWeapon : InstantWeapon
{
var lerp = 1 - unlerp(0, 90, degrees);
_lock = saturate(_lock + pow(lerp, DirectionImpact) * dt * LockSpeed * pow(Entity.EntityInfoGathered[target], SensorImpact));
target.ReceivePaint(Entity.Position, Item?.Data, Lock);
}
else _lock = saturate(_lock - dt * Decay);
}
@ -110,3 +111,18 @@ public class LockWeapon : InstantWeapon
}
}
// What a painted ship senses of one beam on it (ruling sensor-stat-set item 4; strength per ruling
// lock-warning-strength). Strength is the emitter's own lock progress at the paint, 0..1, never summed.
public readonly struct LockWarning
{
public readonly float2 Bearing;
public readonly EquippableItemData EmitterClass;
public readonly float Strength;
public LockWarning(float2 bearing, EquippableItemData emitterClass, float strength)
{
Bearing = bearing;
EmitterClass = emitterClass;
Strength = strength;
}
}

View file

@ -173,6 +173,12 @@ public abstract class Entity
public Subject<Unit> HypothermiaDeath = new Subject<Unit>();
public Subject<Entity> TargetedBy = new Subject<Entity>();
public ReactiveProperty<int> TargetedByCount = new ReactiveProperty<int>(0);
// TargetedBy counts selection, which emits nothing; a lock warning comes only from a paint. LockWeapon.Execute
// reports each tick it paints this ship into the inbox; Update publishes the inbox once, so a warning lives
// exactly one tick after its last paint.
private readonly List<LockWarning> _paintInbox = new List<LockWarning>();
private readonly List<LockWarning> _incomingLocks = new List<LockWarning>();
public IReadOnlyList<LockWarning> IncomingLocks => _incomingLocks;
public ReactiveProperty<SecurityLevel> CurrentSecurityLevel = new ReactiveProperty<SecurityLevel>(SecurityLevel.Open);
public ReadOnlyReactiveProperty<bool> PresencePermitted;
@ -783,6 +789,15 @@ public abstract class Entity
VisibleEntities.Clear();
VisibleEnemies.Clear();
VisibleFriendlies.Clear();
_paintInbox.Clear();
_incomingLocks.Clear();
}
// The only caller is LockWeapon.Execute; strength is the emitter's own Lock at that paint.
internal void ReceivePaint(float3 emitterPosition, EquippableItemData emitterClass, float strength)
{
if (!_active) return;
_paintInbox.Add(new LockWarning(normalize((emitterPosition - Position).xz), emitterClass, strength));
}
public Entity(ItemManager itemManager, Zone zone, EquippableItem hull, EntitySettings settings)
@ -1378,6 +1393,10 @@ public abstract class Entity
TurnRate = 0;
if (!_active) return;
_incomingLocks.Clear();
_incomingLocks.AddRange(_paintInbox);
_paintInbox.Clear();
var velocityBefore = Velocity;
LimitClamp = float2(0, 0);
var hullData = ItemManager.GetData(Hull) as HullData;