From b14d609caed89e062af92674fb46f86796fa6c51 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 21:11:37 +0200 Subject: [PATCH] Schematic lists revealed gear; scan progress indicators at both ends Entity.RevealedGear and SchematicRows' enemy branch (revealed weapons then revealed gear); SchematicRow.Range is float? (null for a non-weapon claim); SchematicDisplay shows ShowGear rows for non-weapon claims and hides the range label when null; SchematicListElement.ShowGear. ActionGameManager: one progress-indicator list (lock weapons hostile-gated, scanners ungated) and the incoming indicator follows Track or Scan paints. Not Unity-compiled yet; Shared tests unaffected by the UI files. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Gameplay/ActionGameManager.cs | 34 ++++++++------ Assets/Scripts/ServerShared/Entity.cs | 21 ++++++--- Assets/Scripts/ServerShared/Sensing.cs | 10 ++-- Assets/Scripts/UI/HUD/SchematicDisplay.cs | 18 ++++--- Assets/Scripts/UI/HUD/SchematicListElement.cs | 47 ++++++++++++++++++- 5 files changed, 96 insertions(+), 34 deletions(-) diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index f7c79f131..0877a4a03 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -203,7 +203,9 @@ public class ActionGameManager : MonoBehaviour private readonly HelmInput _helm = new HelmInput(); private TextMeshProUGUI _faceAimMark; private (HardpointData[] hardpoints, Transform[] barrels, PlaceUIElementWorldspace crosshair)[] _articulationGroups; - private (LockWeapon targetLock, PlaceUIElementWorldspace indicator, Rotate spin)[] _lockingIndicators; + // The outgoing progress indicators on the player's target: one per lock weapon (its Lock, shown only on a hostile target) + // and one per scanner (its scan Progress, shown on any target: a scan reads no stance). + private (Func progress, bool hostileOnly, PlaceUIElementWorldspace indicator, Rotate spin)[] _progressIndicators; private (PlaceUIElementWorldspace indicator, Rotate spin) _incomingLockIndicator; private Dictionary _visibleHostileIndicators = new Dictionary(); private Dictionary _visibleFriendlyIndicators = new Dictionary(); @@ -1021,7 +1023,7 @@ public class ActionGameManager : MonoBehaviour _sweepFlashes.Clear(); _pendingSweeps.Clear(); - if(_lockingIndicators!=null) foreach(var (_, indicator, _) in _lockingIndicators) + if(_progressIndicators!=null) foreach(var (_, _, indicator, _) in _progressIndicators) indicator.GetComponent().ReturnToPool(); if(_incomingLockIndicator.indicator != null) _incomingLockIndicator.indicator.GetComponent().ReturnToPool(); _incomingLockIndicator = default; @@ -1157,11 +1159,13 @@ public class ActionGameManager : MonoBehaviour _shipSubscriptions.Add(CurrentEntity.Death.Subscribe(Die)); - _lockingIndicators = CurrentEntity.GetBehaviors() + var progressSources = CurrentEntity.GetBehaviors().Select(x => ((Func) (() => x.Lock), true)) + .Concat(CurrentEntity.GetBehaviors().Select(x => ((Func) (() => x.Progress), false))); + _progressIndicators = progressSources .Select(x => { var i = LockIndicator.Instantiate(); - return (x, i, i.GetComponent()); + return (x.Item1, x.Item2, i, i.GetComponent()); }).ToArray(); var incoming = LockIndicator.Instantiate(); @@ -1483,24 +1487,26 @@ public class ActionGameManager : MonoBehaviour crosshair.Target = averagePosition; } - foreach (var (targetLock, indicator, spin) in _lockingIndicators) + foreach (var (progress, hostileOnly, indicator, spin) in _progressIndicators) { - var showLockingIndicator = targetLock.Lock > .01f && CurrentEntity.Target.Value.Entity != null && CurrentEntity.IsHostileTo(CurrentEntity.Target.Value.Entity); - indicator.gameObject.SetActive(showLockingIndicator); - if(showLockingIndicator) + var value = progress(); + var targetEntity = CurrentEntity.Target.Value.Entity; + var showIndicator = value > .01f && targetEntity != null && (!hostileOnly || CurrentEntity.IsHostileTo(targetEntity)); + indicator.gameObject.SetActive(showIndicator); + if(showIndicator) { - indicator.Target = CurrentEntity.Target.Value.Entity.Position.ToUnity(); - ApplyLockProgress(indicator, spin, targetLock.Lock); + indicator.Target = targetEntity.Position.ToUnity(); + ApplyLockProgress(indicator, spin, value); } } - // The strongest lock on the player's ship, read only from IncomingPaints (never another ship's LockWeapon). - // Only Track paints are locks; Search paints (ping sweeps) are this class's sweep flashes (UpdateSweepFlashes). - // Same curves as the outgoing indicator, driven by the warning's Strength. + // The strongest lock or scan on the player's ship, read only from IncomingPaints (never another ship's LockWeapon or + // Scanner). A Scan paint's Strength is the scan's progress (ruling scan-is-an-act). Search paints (ping sweeps) are + // this class's sweep flashes (UpdateSweepFlashes). Same curves as the outgoing indicator, driven by the warning's Strength. var warnings = CurrentEntity.IncomingPaints; var strongest = -1; for (var w = 0; w < warnings.Count; w++) - if (warnings[w].Mode == PaintMode.Track && (strongest < 0 || warnings[w].Strength > warnings[strongest].Strength)) strongest = w; + if ((warnings[w].Mode is PaintMode.Track or PaintMode.Scan) && (strongest < 0 || warnings[w].Strength > warnings[strongest].Strength)) strongest = w; var (incomingIndicator, incomingSpin) = _incomingLockIndicator; incomingIndicator.gameObject.SetActive(strongest >= 0); if (strongest >= 0) diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index b0f51a6b7..9c045eb8e 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -1033,8 +1033,15 @@ public abstract class Entity // classifying read of the book for weapons, shared by the enemy schematic and Combat (aetheria-ew, classify-from-belief); // a gun claimed as a non-weapon is not listed, though RevealsOf holds it for aim. public IEnumerable RevealedWeapons(Entity subject) => - RevealsOf(subject).Where(r => r.Claimed != null && r.Claimed.Behaviors.Any(b => b is WeaponData)) - .OrderBy(r => FittedIndex(subject, r.Item)).ThenBy(r => r.Item.EquippableItem.Lot); + InFittedOrder(subject, RevealsOf(subject).Where(r => r.Claimed != null && r.Claimed.Behaviors.Any(b => b is WeaponData))); + + // The gear this observer believes `subject` has: each reveal whose claimed class carries no weapon behaviour and is not a + // hull, in the order of RevealedWeapons. A hull's layout is known, not revealed, so it is never a gear row. + public IEnumerable RevealedGear(Entity subject) => + InFittedOrder(subject, RevealsOf(subject).Where(r => r.Claimed != null && !(r.Claimed is HullData) && !r.Claimed.Behaviors.Any(b => b is WeaponData))); + + private static IEnumerable InFittedOrder(Entity subject, IEnumerable reveals) => + reveals.OrderBy(r => FittedIndex(subject, r.Item)).ThenBy(r => r.Item.EquippableItem.Lot); // The classes `subject` has claimed to this observer, distinct and by name: what a bearing marker is labelled with. Reads // each reveal's Claimed only, never the item's true data (aetheria-ew, classify-from-belief). @@ -1049,14 +1056,14 @@ public abstract class Entity return index < 0 ? int.MaxValue : index; } - // The weapon rows of `subject`'s schematic as this ship reads it: its own equipment when `subject` is this ship (each - // row its own data and live range, nothing to doubt about your own ship), otherwise RevealedWeapons (each row the - // claimed class and that design's rated range). The one owner of which rows, which class and which range an - // enemy schematic shows; the Unity SchematicDisplay renders it. + // The rows of `subject`'s schematic as this ship reads it: its own weapons when `subject` is this ship (each row its + // own data and live range, nothing to doubt about your own ship), otherwise the revealed weapons (each row the claimed + // class and that design's rated range) then the revealed gear (each row the claimed class, no range). The one owner of + // which rows, which class and which range an enemy schematic shows; the Unity SchematicDisplay renders it. public IEnumerable SchematicRows(Entity subject) => subject == this ? subject.Equipment.Where(i => i.Behaviors.Any(b => b.Data is WeaponData)).Select(i => new SchematicRow(i, i.Data, true)) - : RevealedWeapons(subject).Select(r => new SchematicRow(r.Item, r.Claimed, false)); + : RevealedWeapons(subject).Concat(RevealedGear(subject)).Select(r => new SchematicRow(r.Item, r.Claimed, false)); // Whether this observer's book for `subject` holds `item`: the book's membership, no live gear read. internal bool HasReveal(Entity subject, EquippedItem item) => diff --git a/Assets/Scripts/ServerShared/Sensing.cs b/Assets/Scripts/ServerShared/Sensing.cs index da1144a75..526a8db23 100644 --- a/Assets/Scripts/ServerShared/Sensing.cs +++ b/Assets/Scripts/ServerShared/Sensing.cs @@ -122,11 +122,11 @@ public readonly struct Reveal } } -// One weapon row of a ship's schematic as its viewer is entitled to read it (aetheria-ew, classify-from-belief; ruling +// One weapon or gear row of a ship's schematic as its viewer is entitled to read it (aetheria-ew, classify-from-belief; ruling // ew-claim-weapon-measure-ruled). Made only by Entity.SchematicRows. `Item` is the true item: the selection mark's key and, // on the viewer's own ship, the live gear. `Claimed` is the class the row shows. The range label is the live range on -// the viewer's own ship and the claimed design's rated range on any other; the viewer never sees a target weapon's -// quality, wear or power. A renderer shows these and decides nothing. +// the viewer's own ship and the claimed design's rated range on any other; a row whose claimed class is not a weapon has +// no range. The viewer never sees a target item's quality, wear or power. A renderer shows these and decides nothing. public readonly struct SchematicRow { public readonly EquippedItem Item; @@ -143,9 +143,9 @@ public readonly struct SchematicRow // What a renderer compares to know the rows changed. Range is a reading, not part of the key. public (EquippedItem, EquippableItemData) Key => (Item, Claimed); - public float Range => _own + public float? Range => _own ? Item.Behaviors.OfType().First().Range - : Claimed.Behaviors.OfType().First().RatedRange; + : Claimed.Behaviors.OfType().FirstOrDefault()?.RatedRange; } // A subject's removal from a zone as the zone recorded it (death, dock, warp, any removal): where it was and how diff --git a/Assets/Scripts/UI/HUD/SchematicDisplay.cs b/Assets/Scripts/UI/HUD/SchematicDisplay.cs index dc0740cf2..1ac5a23a7 100644 --- a/Assets/Scripts/UI/HUD/SchematicDisplay.cs +++ b/Assets/Scripts/UI/HUD/SchematicDisplay.cs @@ -94,12 +94,12 @@ public class SchematicDisplay : MonoBehaviour CargoTemperatureLabel.text = "N/A"; } - BuildRows(WeaponRows()); + BuildRows(Rows()); } // Which rows, which class and which range label are Entity.SchematicRows' decision (aetheria-ew, classify-from-belief); // this display only renders them. - private IEnumerable WeaponRows() => (_player ?? _entity).SchematicRows(_entity); + private IEnumerable Rows() => (_player ?? _entity).SchematicRows(_entity); private void ReturnRows() { @@ -125,6 +125,8 @@ public class SchematicDisplay : MonoBehaviour { if(x.Row.Claimed is WeaponItemData weaponItemData) x.ListElement.ShowWeapon(weaponItemData); + else + x.ListElement.ShowGear(x.Row.Claimed.HardpointType, x.Row.Claimed.Name); //x.ListElement.Label.text = x.Item.EquippableItem.Name; if (!_enemy) { @@ -194,11 +196,11 @@ public class SchematicDisplay : MonoBehaviour // } // A reveal gained or ended after selection changes the rows with no event to hook: rebuild when the - // revealed weapon set differs from the rows' items. - if (_enemy && !WeaponRows().Select(r => r.Key).SequenceEqual(_schematicItems.Select(x => x.Row.Key))) + // revealed set differs from the rows' items. + if (_enemy && !Rows().Select(r => r.Key).SequenceEqual(_schematicItems.Select(x => x.Row.Key))) { ReturnRows(); - BuildRows(WeaponRows()); + BuildRows(Rows()); } // Cut 2 (docs/fire-control-cut.md): marks whichever row is the player's current aim point. @@ -239,7 +241,11 @@ public class SchematicDisplay : MonoBehaviour } if (x.ListElement.RangeLabel) - x.ListElement.RangeLabel.text = ((int) x.Row.Range).ToString(); + { + var range = x.Row.Range; + x.ListElement.RangeLabel.gameObject.SetActive(range.HasValue); + if (range.HasValue) x.ListElement.RangeLabel.text = ((int) range.Value).ToString(); + } } } } diff --git a/Assets/Scripts/UI/HUD/SchematicListElement.cs b/Assets/Scripts/UI/HUD/SchematicListElement.cs index 5603e927e..e5433c52f 100644 --- a/Assets/Scripts/UI/HUD/SchematicListElement.cs +++ b/Assets/Scripts/UI/HUD/SchematicListElement.cs @@ -22,6 +22,49 @@ public class SchematicListElement : MonoBehaviour private List _iconInstances = new List(); + // A gear row names its class; created on first use as VisibleTargetIndicator.ShowLabel creates its own (no scene or + // prefab edit) and hidden by every other row kind, because pools are shared. + private TextMeshProUGUI _nameLabel; + + private void ClearIcons() + { + foreach(var prototype in _iconInstances) prototype.ReturnToPool(); + _iconInstances.Clear(); + } + + private void SetName(string name) + { + var show = !string.IsNullOrEmpty(name); + if (_nameLabel == null) + { + if (!show) return; + var child = new GameObject("Name", typeof(RectTransform)); + child.transform.SetParent(transform, false); + _nameLabel = child.AddComponent(); + _nameLabel.fontSize = 14; + _nameLabel.alignment = TextAlignmentOptions.Left; + _nameLabel.raycastTarget = false; + var rect = _nameLabel.rectTransform; + rect.anchorMin = new Vector2(0, 0); + rect.anchorMax = new Vector2(1, 1); + rect.offsetMin = new Vector2(40, 0); + rect.offsetMax = Vector2.zero; + } + _nameLabel.gameObject.SetActive(show); + if (show && _nameLabel.text != name) _nameLabel.text = name; + } + + // A revealed item that is not a weapon: its hardpoint's icon and, when given, its class name. Layout-aim reuses this with + // a null name for mount rows. + public void ShowGear(HardpointType type, string name) + { + ClearIcons(); + var icon = IconPrototype.Instantiate(); + _iconInstances.Add(icon.GetComponent()); + icon.sprite = Settings.GetIcon(type); + SetName(name); + } + public void SetSelected(bool selected) { if (SelectedIndicator != null) SelectedIndicator.SetActive(selected); @@ -29,8 +72,8 @@ public class SchematicListElement : MonoBehaviour public void ShowWeapon(WeaponItemData weapon) { - foreach(var prototype in _iconInstances) prototype.ReturnToPool(); - _iconInstances.Clear(); + ClearIcons(); + SetName(null); var caliberIcon = IconPrototype.Instantiate(); _iconInstances.Add(caliberIcon.GetComponent());