From c4a57a8df0854e7e9e976fd96854f99c70631e0b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 19 Sep 2026 13:02:05 +0200 Subject: [PATCH] Add targeting subsystem, reveal rule and selection slot (fire control Cut 2) TargetingSystem is required interior Tool gear: Accuracy/Resolution/ Precision/Tracking are PerformanceStats reached through the usual Evaluate path, so lot quality, wear, heat and PowerSupply all apply (a starved system rolls worse, never switches off). FireControl.Accuracy/ Resolution/Precision fall back to GameplaySettings.UnaidedAccuracy (new, authored low per Q4) whenever an entity has no targeting system or its own is destroyed/disabled. FireControl.IsRevealed derives R5's reveal rule from EntityInfoGathered: non-hull equipment ranks hardpoint-mounted first, then by size, and reveals between TargetArmorInfoThreshold and TargetGearInfoThreshold -- the first code to read either setting. Entity.TargetItem/ TrySelectTargetItem/ResolvedTargetItem give both the player and AI one shared, revoke-on-decay aim-point slot with a single writer. Co-Authored-By: Claude Opus 5 --- .../ServerShared/Behaviors/Behaviors.cs | 3 + .../ServerShared/Behaviors/TargetingSystem.cs | 77 +++++++++++++++++++ .../Behaviors/TargetingSystem.cs.meta | 3 + Assets/Scripts/ServerShared/Entity.cs | 40 +++++++++- Assets/Scripts/ServerShared/FireControl.cs | 62 ++++++++++++++- Assets/Scripts/ServerShared/Settings.cs | 8 ++ 6 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 Assets/Scripts/ServerShared/Behaviors/TargetingSystem.cs create mode 100644 Assets/Scripts/ServerShared/Behaviors/TargetingSystem.cs.meta diff --git a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs index 7e44c96e0..b9c4c152e 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs @@ -183,6 +183,9 @@ public interface IPowerConsumer Union(36, typeof(ConstantWeaponData)), Union(37, typeof(ChargedWeaponData)), Union(38, typeof(AutoWeaponData)), + // Cut 2 (docs/fire-control-cut.md): 39 is the next free index against this list, not the 39 the + // superseded draft quoted -- checked directly here rather than trusted from that document. + Union(39, typeof(TargetingSystemData)), JsonObject(MemberSerialization.OptIn)] public abstract class BehaviorData { diff --git a/Assets/Scripts/ServerShared/Behaviors/TargetingSystem.cs b/Assets/Scripts/ServerShared/Behaviors/TargetingSystem.cs new file mode 100644 index 000000000..4cf999279 --- /dev/null +++ b/Assets/Scripts/ServerShared/Behaviors/TargetingSystem.cs @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using MessagePack; +using Newtonsoft.Json; + +// Cut 2 (docs/fire-control-cut.md): the required interior Tool gear that makes aimed fire possible at all +// (R2, Q4). An entity with no working targeting system is not merely worse -- FireControl falls back to +// GameplaySettings.UnaidedAccuracy and a Precision of 0, which is authored to make unaided fire a last +// resort (Q4: "really, really bad"). This behaviour carries no power machinery of its own: EnergyDraw and +// Heat, authored alongside it on the same item, already cover that, and it adds nothing new. +[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable] +public class TargetingSystemData : BehaviorData +{ + // The ceiling on hit probability (Cut 3's roll multiplies against this; nothing here computes a + // probability). A stat, not a constant, so lot quality, wear, heat and PowerSupply all reach it through + // EquippedItem.Evaluate -- a starved targeting system rolls worse rather than switching off (the + // brownout ruling, docs/stats-and-power-cut.md, applied here). + [Inspectable, JsonProperty("accuracy"), Key(1), RuntimeInspectable] + public PerformanceStat Accuracy = new PerformanceStat(); + + // The info level (see Entity.EntityInfoGathered) at which sensor state stops limiting hits. Below the + // system's own resolution, an otherwise-earned hit still degrades on account of thin sensor data; + // consumed by Cut 3's roll. + [Inspectable, JsonProperty("resolution"), Key(2), RuntimeInspectable] + public PerformanceStat Resolution = new PerformanceStat(); + + // The chance a hit lands on the item actually aimed at (Entity.TargetItem) rather than a random cell. + // Consumed by Cut 3's roll. + [Inspectable, JsonProperty("precision"), Key(3), RuntimeInspectable] + public PerformanceStat Precision = new PerformanceStat(); + + // How much target-side deviation from the predicted intercept this system forgives before a shot goes + // wide. Consumed by Cut 3's roll; unused until then. + [Inspectable, JsonProperty("tracking"), Key(4), RuntimeInspectable] + public PerformanceStat Tracking = new PerformanceStat(); + + public override Behavior CreateInstance(EquippedItem item) + { + return new TargetingSystem(this, item); + } + + public override Behavior CreateInstance(ConsumableItemEffect item) + { + return new TargetingSystem(this, item); + } +} + +public class TargetingSystem : Behavior +{ + private readonly TargetingSystemData _data; + + public float Accuracy { get; private set; } + public float Resolution { get; private set; } + public float Precision { get; private set; } + public float Tracking { get; private set; } + + public TargetingSystem(TargetingSystemData data, EquippedItem item) : base(data, item) + { + _data = data; + } + + public TargetingSystem(TargetingSystemData data, ConsumableItemEffect item) : base(data, item) + { + _data = data; + } + + public override bool Execute(float dt) + { + Accuracy = Evaluate(_data.Accuracy); + Resolution = Evaluate(_data.Resolution); + Precision = Evaluate(_data.Precision); + Tracking = Evaluate(_data.Tracking); + return true; + } +} diff --git a/Assets/Scripts/ServerShared/Behaviors/TargetingSystem.cs.meta b/Assets/Scripts/ServerShared/Behaviors/TargetingSystem.cs.meta new file mode 100644 index 000000000..63ea1ca6c --- /dev/null +++ b/Assets/Scripts/ServerShared/Behaviors/TargetingSystem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e72dd6e15fead417f201fdc92c1fd259 +timeCreated: 1758283200 diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 585c5e534..b1c22245a 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -45,6 +45,13 @@ public abstract class Entity public List Children = new List(); public ReactiveProperty Target = new ReactiveProperty((Entity)null); + // Cut 2 (docs/fire-control-cut.md): the stored aim-point slot. Written only by TrySelectTargetItem + // below, and nulled whenever Target changes (subscribed in Activate). Do not read this field directly + // outside Entity -- read ResolvedTargetItem, which re-validates on every call, so decay (or the item + // leaving the target) drops the aim point the moment it is no longer earned, with no loop that has to + // notice and clear it. + public ReactiveProperty TargetItem = new ReactiveProperty((EquippedItem)null); + public float3 LookDirection; public string Name; @@ -273,11 +280,42 @@ public abstract class Entity foreach(var subscription in _watchedEntitySubscriptions[disappearingEntity.Value]) subscription.Dispose(); _watchedEntitySubscriptions.Remove(disappearingEntity.Value); })); - + + // Cut 2 (docs/fire-control-cut.md): a Target change nulls the aim point. Fires immediately on + // subscribe with whatever Target already holds, which is harmless -- TargetItem starts null anyway. + _subscriptions.Add(Target.Subscribe(_ => TargetItem.Value = null)); + if(WeaponGroups.All(wg=>!wg.items.Any())) GenerateWeaponGroups(); } + // Cut 2 (docs/fire-control-cut.md): the only writer of TargetItem. Accepts null (clearing the aim + // point) or an item belonging to the current Target that FireControl.IsRevealed resolves true for this + // entity as observer. Returns whether the write took effect; a rejected non-null selection leaves + // TargetItem unchanged. + public bool TrySelectTargetItem(EquippedItem item) + { + if (item == null) + { + TargetItem.Value = null; + return true; + } + + if (Target.Value == null || item.Entity != Target.Value || !FireControl.IsRevealed(this, item)) + return false; + + TargetItem.Value = item; + return true; + } + + // The read path for TargetItem (see the field's own comment): null once the aimed item is no longer + // revealed to this entity, or no longer belongs to the current Target, even though nothing wrote + // TargetItem.Value at the moment that became true. Re-checked on every call; never cached. + public EquippedItem ResolvedTargetItem => + TargetItem.Value != null && Target.Value == TargetItem.Value.Entity && FireControl.IsRevealed(this, TargetItem.Value) + ? TargetItem.Value + : null; + // Another entity's stance toward THIS one, as far as this entity can perceive it: unknown (null) // until this entity detects them, mirroring the existing detection model (VisibleEntities, driven // by EntityInfoGathered crossing TargetDetectionInfoThreshold). Detected but not yet hostility-rated diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index a19bf5838..19089b7d0 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -2,13 +2,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +using System.Linq; using CultMath; using static CultMath.math; -// Cut 1 (docs/fire-control-cut.md): geometry only. Whether a weapon bears on a point is decided here and +// Cut 1 (docs/fire-control-cut.md): geometry -- whether a weapon bears on a point is decided here and // nowhere else -- no prefab, transform, ArticulationPoint or renderer may influence it (R6, R7). Mount // direction and arc are both derived per read from catalog data and the item's authored rotation; neither // is ever stored on the entity. +// Cut 2 adds the reveal rule (R5) and the resolved targeting-system stats an entity fires with, both +// likewise derived on every read and never cached. public static class FireControl { // The planar unit vector a hardpoint's weapon points along, derived solely from the item's authored @@ -40,4 +43,61 @@ public static class FireControl var planarTarget = normalize(float3(toTarget.x, 0, toTarget.z)); return dot(MountDirection(weapon), planarTarget) >= cos(radians(arc / 2f)); } + + // Cut 2, R5: whether `observer` has resolved `item` (which must belong to some entity in the zone) well + // enough to aim at it. Derived fresh from EntityInfoGathered on every call -- nothing caches a reveal + // result, so decay drops it the moment info falls back below tier, with no clearing loop anywhere. + // + // Ranking: the target's own non-hull equipment, hardpoint-mounted gear first (the big obvious things), + // then by size descending, ties broken by equipment order (OrderByDescending is a stable sort, so this + // needs no explicit tie-break key). Item i of N reveals once info crosses + // lerp(TargetArmorInfoThreshold, TargetGearInfoThreshold, i / max(1, N - 1)) -- the first item to reveal + // does so at the armor tier, the last at the gear tier. + public static bool IsRevealed(Entity observer, EquippedItem item) + { + var target = item.Entity; + var ranked = target.Equipment + .Where(e => e.Data.HardpointType != HardpointType.Hull) + .OrderByDescending(e => e.Data.HardpointType != HardpointType.Tool) + .ThenByDescending(e => e.Data.Shape.Coordinates.Length) + .ToList(); + var index = ranked.IndexOf(item); + if (index < 0) return false; // the hull itself, or an item that is no longer on this target + + var settings = observer.ItemManager.GameplaySettings; + var info = observer.EntityInfoGathered.TryGetValue(target, out var gathered) ? gathered : 0f; + var tier = lerp(settings.TargetArmorInfoThreshold, settings.TargetGearInfoThreshold, + (float) index / max(1, ranked.Count - 1)); + return info >= tier; + } + + // Cut 2, Q4: the stats an entity actually fires with. A working targeting system (equipped, online -- + // not destroyed, not shut down) supplies its own resolved stats; anything else falls back to the + // authored unaided figures, which make aiming without one a last resort rather than an alternative. + // Cut 3's roll is the only planned reader. + public static float Accuracy(Entity entity) + { + var system = entity.GetBehavior(); + return system != null && system.Item.Active.Value + ? system.Accuracy + : entity.ItemManager.GameplaySettings.UnaidedAccuracy; + } + + public static float Resolution(Entity entity) + { + var system = entity.GetBehavior(); + return system != null && system.Item.Active.Value ? system.Resolution : 1f; + } + + public static float Precision(Entity entity) + { + var system = entity.GetBehavior(); + return system != null && system.Item.Active.Value ? system.Precision : 0f; + } + + public static float Tracking(Entity entity) + { + var system = entity.GetBehavior(); + return system != null && system.Item.Active.Value ? system.Tracking : 0f; + } } diff --git a/Assets/Scripts/ServerShared/Settings.cs b/Assets/Scripts/ServerShared/Settings.cs index 41126bc79..32426fc29 100644 --- a/Assets/Scripts/ServerShared/Settings.cs +++ b/Assets/Scripts/ServerShared/Settings.cs @@ -213,6 +213,14 @@ public class GameplaySettings public float TargetGearInfoThreshold; public float ConvergenceMinimumDistance; public float FiringArc = 120; + // Cut 2 (docs/fire-control-cut.md, Q4): what an entity with no working targeting system fires with -- + // the ceiling FireControl.Accuracy falls back to (Resolution 1, Precision 0 alongside it). Authored low + // on purpose ("really, really bad," operator 2026-09-19): .05 against the two catalog designs' own + // Accuracy ranges (the authoring spec this cut produces proposes .45-.6 for the 1-cell design and + // .65-.85 for the 2-cell one) leaves unaided fire capable of hitting something close, slow and + // unaware, and little else. One authored setting, easy to find and turn -- the operator rules the real + // figure in play. + public float UnaidedAccuracy = .05f; public float AgentRangeExponent = .25f; public float AgentForwardLerp = .5f; public float AgentMaxForwardDistance = 50;