Merge eureka/aetheria-release-contact-reveals: reveals end on perception (cut-contact-reveals.r2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
31d19eae64
12 changed files with 1486 additions and 24 deletions
|
|
@ -290,7 +290,7 @@ public class EntityInstance : MonoBehaviour
|
|||
|
||||
_subscriptions.Add(Entity.HullDamage.Subscribe(_ =>
|
||||
{
|
||||
if (Entity.Hull.Durability < .01f)
|
||||
if (Entity.Hull.IsDestroyed)
|
||||
{
|
||||
if (!this) return;
|
||||
_destroyed = true;
|
||||
|
|
|
|||
|
|
@ -102,7 +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, PaintMode.Track, Lock);
|
||||
target.ReceivePaint(Entity, Item, PaintMode.Track, Lock);
|
||||
}
|
||||
else _lock = saturate(_lock - dt * Decay);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
if (sweeping && dist < _pingRadius && _reached.Add(entity) && InBeam(diff, dist))
|
||||
{
|
||||
heard = true;
|
||||
entity.ReceivePaint(Entity.Position, Item?.Data, PaintMode.Search, 0);
|
||||
entity.ReceivePaint(Entity, Item, PaintMode.Search, 0);
|
||||
credit += Gain(entity.Visibility, sensitivity, response, dist, dt, true, obscuration);
|
||||
var pingReturn = new PingReturn(entity, dist, obscuration, Evaluate(_data.PingRange));
|
||||
foreach (var ally in Entity.Zone.Entities)
|
||||
|
|
|
|||
|
|
@ -189,7 +189,22 @@ public abstract class Entity
|
|||
// sweeping this ship (Sensor.Execute); a track paint is a lock (LockWeapon.Execute). Each reports into the
|
||||
// inbox the tick it paints; PublishInbox publishes the inbox once, so a warning lives exactly one tick after
|
||||
// its last paint.
|
||||
private readonly List<PaintWarning> _paintInbox = new List<PaintWarning>();
|
||||
// The inbox keeps who painted and with what beside the warning: the warning is all a reader of IncomingPaints
|
||||
// gets, the emitter and item are for PublishInbox, which records the emitter's reveal.
|
||||
private readonly struct PendingPaint
|
||||
{
|
||||
public readonly PaintWarning Warning;
|
||||
public readonly Entity Emitter;
|
||||
public readonly EquippedItem Item;
|
||||
|
||||
public PendingPaint(PaintWarning warning, Entity emitter, EquippedItem item)
|
||||
{
|
||||
Warning = warning;
|
||||
Emitter = emitter;
|
||||
Item = item;
|
||||
}
|
||||
}
|
||||
private readonly List<PendingPaint> _paintInbox = new List<PendingPaint>();
|
||||
private readonly List<PaintWarning> _incomingPaints = new List<PaintWarning>();
|
||||
public IReadOnlyList<PaintWarning> IncomingPaints => _incomingPaints;
|
||||
// An ally's sensor posts a return on the tick its ping crosses a subject; published with paints, read by this
|
||||
|
|
@ -197,6 +212,12 @@ public abstract class Entity
|
|||
private readonly List<PingReturn> _returnInbox = new List<PingReturn>();
|
||||
private readonly List<PingReturn> _pingReturns = new List<PingReturn>();
|
||||
internal IReadOnlyList<PingReturn> PingReturns => _pingReturns;
|
||||
// What this observer has seen each ship's items do (ruling visibility-per-capability-reveal), keyed (subject,
|
||||
// item). Written only by RecordReveal (an act enters) and EndReveals (an end the observer perceives, or a departed
|
||||
// subject's entries aged past BearingMemory), cleared by Deactivate (ruling reveal-ends-on-perception).
|
||||
private readonly Dictionary<Entity, Dictionary<EquippedItem, Reveal>> _reveals = new Dictionary<Entity, Dictionary<EquippedItem, Reveal>>();
|
||||
private readonly List<EquippedItem> _endedItems = new List<EquippedItem>();
|
||||
private readonly List<Entity> _agedSubjects = new List<Entity>();
|
||||
public ReactiveProperty<SecurityLevel> CurrentSecurityLevel = new ReactiveProperty<SecurityLevel>(SecurityLevel.Open);
|
||||
public ReadOnlyReactiveProperty<bool> PresencePermitted;
|
||||
|
||||
|
|
@ -395,22 +416,30 @@ public abstract class Entity
|
|||
|
||||
// Mining Cut 3 (Q14 A): this entity's info on a chunk, computed when asked and stored nowhere. It is the value
|
||||
// the per-tick sensor rule (Sensor.Execute, through Sensor.Gain) would settle at for a target this bright,
|
||||
// here, now. Each active sensor applies x <- saturate(x + G_i dt)(1 - k dt) every tick, so with n sensors the
|
||||
// info settles where the gain balances the decay: sum(G_i) / (n k), capped at 1. A ping adds nothing that
|
||||
// lasts, so only the passive gain enters.
|
||||
// here, now (PassiveInfo at the chunk's brightness), capped at 1.
|
||||
public float ChunkInfo(ChunkId chunk)
|
||||
{
|
||||
var visibility = Zone.ChunkVisibility(chunk);
|
||||
if (visibility <= 0f) return 0f;
|
||||
var position = Zone.ChunkPose(chunk.Field, chunk.Index).xy;
|
||||
return saturate(PassiveInfo(Zone.ChunkPose(chunk.Field, chunk.Index).xy, visibility));
|
||||
}
|
||||
|
||||
// The info a target of `signature` at `position` would settle at under this entity's passive sensing, before
|
||||
// the cap at 1. Each active sensor applies x <- saturate(x + G_i dt)(1 - k dt) every tick, so with n sensors the
|
||||
// info settles where the gain balances the decay: sum(G_i) / (n k). A ping adds nothing that lasts, so only the
|
||||
// passive gain enters. ChunkInfo asks it for a chunk's brightness; Sense asks it for a unit signature once per
|
||||
// ship, because it is linear in the signature below saturation and so answers for every act at a place. Zero
|
||||
// with no active sensor.
|
||||
public float PassiveInfo(float2 position, float signature)
|
||||
{
|
||||
var rate = 0f;
|
||||
var sensors = 0;
|
||||
foreach (var sensor in ActiveSensors())
|
||||
{
|
||||
rate += sensor.PassiveRate(visibility, position);
|
||||
rate += sensor.PassiveRate(signature, position);
|
||||
sensors++;
|
||||
}
|
||||
return sensors == 0 ? 0f : saturate(rate / (sensors * ItemManager.GameplaySettings.TargetInfoDecay));
|
||||
return sensors == 0 ? 0f : rate / (sensors * ItemManager.GameplaySettings.TargetInfoDecay);
|
||||
}
|
||||
|
||||
// Mining Cut 3: the detection threshold, read through the chunk path. A chunk is visible exactly as an entity
|
||||
|
|
@ -637,10 +666,25 @@ public abstract class Entity
|
|||
return max(gain, 0f) / (sensors * ItemManager.GameplaySettings.TargetInfoDecay * threshold) * 1.0001f;
|
||||
}
|
||||
|
||||
// Every sensor behaviour that runs this tick: those on active equipment, and those of active consumables.
|
||||
private IEnumerable<Sensor> ActiveSensors() =>
|
||||
Equipment.Where(item => item.Active.Value).SelectMany(item => item.Behaviors).OfType<Sensor>()
|
||||
.Concat(_activeConsumables.SelectMany(effect => effect.Behaviors).OfType<Sensor>());
|
||||
// Every sensor behaviour that runs this tick: those on active equipment, and those of active consumables. The
|
||||
// list is this entity's one buffer, refilled per call (Sense asks once per other ship per tick, allocating
|
||||
// nothing), so callers use it before asking again.
|
||||
private readonly List<Sensor> _activeSensors = new List<Sensor>();
|
||||
private List<Sensor> ActiveSensors()
|
||||
{
|
||||
_activeSensors.Clear();
|
||||
for (var i = 0; i < Equipment.Count; i++)
|
||||
{
|
||||
var item = Equipment[i];
|
||||
if (!item.Active.Value) continue;
|
||||
foreach (var behavior in item.Behaviors)
|
||||
if (behavior is Sensor sensor) _activeSensors.Add(sensor);
|
||||
}
|
||||
foreach (var effect in _activeConsumables)
|
||||
foreach (var behavior in effect.Behaviors)
|
||||
if (behavior is Sensor sensor) _activeSensors.Add(sensor);
|
||||
return _activeSensors;
|
||||
}
|
||||
|
||||
// Cut 12.3 (docs/fire-control-cut.md): "armour absorbs first" -- the per-cell armour phase, split out of
|
||||
// what used to be Absorb's own first half (moved verbatim from DamageSchematic's per-cell body, Cut 3) so
|
||||
|
|
@ -811,6 +855,7 @@ public abstract class Entity
|
|||
_incomingPaints.Clear();
|
||||
_returnInbox.Clear();
|
||||
_pingReturns.Clear();
|
||||
_reveals.Clear();
|
||||
}
|
||||
|
||||
// Zone.Update runs this on every entity before any entity updates, so a paint made during tick N is published
|
||||
|
|
@ -819,7 +864,11 @@ public abstract class Entity
|
|||
internal void PublishInbox()
|
||||
{
|
||||
_incomingPaints.Clear();
|
||||
_incomingPaints.AddRange(_paintInbox);
|
||||
foreach (var paint in _paintInbox)
|
||||
{
|
||||
_incomingPaints.Add(paint.Warning);
|
||||
RecordReveal(paint.Emitter, paint.Item, SignatureChannel.Emission);
|
||||
}
|
||||
_paintInbox.Clear();
|
||||
_pingReturns.Clear();
|
||||
_pingReturns.AddRange(_returnInbox);
|
||||
|
|
@ -833,11 +882,126 @@ public abstract class Entity
|
|||
}
|
||||
|
||||
// The callers are Sensor.Execute (a Search paint, Strength 0) and LockWeapon.Execute (a Track paint, Strength
|
||||
// the emitter's own Lock at that paint).
|
||||
internal void ReceivePaint(float3 emitterPosition, EquippableItemData emitterClass, PaintMode mode, float strength)
|
||||
// the emitter's own Lock at that paint). The emitter's class is the item's data (null with no item: a
|
||||
// consumable's effect, which has no item and so is never revealed).
|
||||
internal void ReceivePaint(Entity emitter, EquippedItem item, PaintMode mode, float strength)
|
||||
{
|
||||
if (!_active) return;
|
||||
_paintInbox.Add(new PaintWarning(normalize((emitterPosition - Position).xz), emitterClass, mode, strength));
|
||||
_paintInbox.Add(new PendingPaint(new PaintWarning(normalize((emitter.Position - Position).xz), item?.Data, mode, strength), emitter, item));
|
||||
}
|
||||
|
||||
// The reveal book's one way in and its one membership gate (callers: Sense and PublishInbox, both in this file):
|
||||
// this observer saw `item` of `subject` act on `channel`, now, from this bearing. An item-less act (hull
|
||||
// radiation, a consumable) reveals nothing, and neither does a subject that is not in the zone: a departed
|
||||
// subject's entries are never created or refreshed, from any caller, and neither does an item this observer
|
||||
// perceives destroyed (PerceivesDestroyed). A later act of the same item replaces the
|
||||
// earlier one.
|
||||
internal void RecordReveal(Entity subject, EquippedItem item, SignatureChannel channel)
|
||||
{
|
||||
if (!_active || item == null || !Zone.Entities.Contains(subject) || PerceivesDestroyed(subject, item)) return;
|
||||
if (!_reveals.TryGetValue(subject, out var book))
|
||||
_reveals[subject] = book = new Dictionary<EquippedItem, Reveal>();
|
||||
book[item] = new Reveal(item, channel, normalize((subject.Position - Position).xz), Zone.Time);
|
||||
}
|
||||
|
||||
// This observer resolves `subject`'s gear (ContactOf's Identified gate) and `item` of it is destroyed: the one rule
|
||||
// for a destroyed item an observer perceives, read by the book's entry (RecordReveal) and its end (Sense).
|
||||
private bool PerceivesDestroyed(Entity subject, EquippedItem item) =>
|
||||
item.EquippableItem.IsDestroyed
|
||||
&& EntityInfoGathered.TryGetValue(subject, out var info) && info >= ItemManager.GameplaySettings.TargetGearInfoThreshold;
|
||||
|
||||
// The book's one way out (caller: Sense): `item` null ends every reveal of `subject`, otherwise that item's.
|
||||
// The subject's key goes with its last entry.
|
||||
private void EndReveals(Entity subject, EquippedItem item)
|
||||
{
|
||||
if (!_reveals.TryGetValue(subject, out var book)) return;
|
||||
if (item != null) book.Remove(item);
|
||||
else book.Clear();
|
||||
if (book.Count == 0) _reveals.Remove(subject);
|
||||
}
|
||||
|
||||
// What this observer has seen `subject`'s items do: each item's latest act, and nothing the subject's present
|
||||
// gear or Durability says (ruling contact-reads-book-aim-reads-live). A reveal ends only on what this observer
|
||||
// perceives (ruling reveal-ends-on-perception): the destruction of an item of a ship it identifies, the death,
|
||||
// dock or warp of a subject its instruments could see leave, or, for a subject that has left the zone, the
|
||||
// freshest entry aging past BearingMemory; Deactivate clears it all. Whether an item can be aimed at now is
|
||||
// FireControl.IsRevealed's question, not this one.
|
||||
public IEnumerable<Reveal> RevealsOf(Entity subject) =>
|
||||
_reveals.TryGetValue(subject, out var book) ? book.Values : Enumerable.Empty<Reveal>();
|
||||
|
||||
// The one derivation of how much this observer holds of `subject`: see Contact.
|
||||
public Contact ContactOf(Entity subject)
|
||||
{
|
||||
var settings = ItemManager.GameplaySettings;
|
||||
var info = EntityInfoGathered.TryGetValue(subject, out var gathered) ? gathered : 0f;
|
||||
var tracked = VisibleEntities.Contains(subject);
|
||||
var any = false;
|
||||
var freshest = default(Reveal);
|
||||
foreach (var reveal in RevealsOf(subject))
|
||||
if (!any || reveal.Time > freshest.Time)
|
||||
{
|
||||
any = true;
|
||||
freshest = reveal;
|
||||
}
|
||||
var remembered = any && Zone.Time - freshest.Time < settings.BearingMemory;
|
||||
var detected = tracked || remembered;
|
||||
var bearing = tracked ? normalize((subject.Position - Position).xz) : remembered ? freshest.Bearing : default;
|
||||
return new Contact(detected, tracked, detected && any, info >= settings.TargetGearInfoThreshold, bearing, info);
|
||||
}
|
||||
|
||||
// What this observer's instruments receive of other ships' acts this tick (Zone.Update calls it once, after every
|
||||
// entity has updated): every light signature of an item that would alone settle this observer's passive info above
|
||||
// TargetDetectionInfoThreshold is revealed (ruling visibility-per-capability-reveal). Hull radiation names no
|
||||
// item and only feeds the track; an Emission entry is never light. One PassiveInfo per ship. Zone.Update calls it
|
||||
// for every entity after all of them have updated, so what a ship shows is what every observer receives on that
|
||||
// tick whatever order the zone lists them in.
|
||||
internal void Sense()
|
||||
{
|
||||
if (!_active) return;
|
||||
var threshold = ItemManager.GameplaySettings.TargetDetectionInfoThreshold;
|
||||
var entities = Zone.Entities;
|
||||
for (var i = 0; i < entities.Count; i++)
|
||||
{
|
||||
var other = entities[i];
|
||||
if (other == this) continue;
|
||||
var perSignature = -1f;
|
||||
foreach (var signature in other._visibilitySources.Values)
|
||||
{
|
||||
if (signature.Item == null || signature.Channel == SignatureChannel.Emission) continue;
|
||||
if (perSignature < 0f) perSignature = PassiveInfo(other.Position.xz, 1f);
|
||||
if (signature.Strength * perSignature > threshold) RecordReveal(other, signature.Item, signature.Channel);
|
||||
}
|
||||
// The observer resolves this ship's gear (PerceivesDestroyed), so it perceives an item of it destroyed and
|
||||
// that reveal ends here; RecordReveal refuses the same item's lingering flash.
|
||||
if (_reveals.TryGetValue(other, out var book))
|
||||
{
|
||||
foreach (var item in book.Keys)
|
||||
if (PerceivesDestroyed(other, item)) _endedItems.Add(item);
|
||||
for (var j = 0; j < _endedItems.Count; j++) EndReveals(other, _endedItems[j]);
|
||||
_endedItems.Clear();
|
||||
}
|
||||
}
|
||||
// A subject that left the zone (death, dock, warp, any removal) is perceived gone under the light-act gate: as
|
||||
// bright as its non-Emission signature was at removal, from where it was. A re-admitted subject did not leave.
|
||||
var departures = Zone.Departures;
|
||||
for (var i = 0; i < departures.Count; i++)
|
||||
{
|
||||
var departure = departures[i];
|
||||
if (!_reveals.ContainsKey(departure.Subject) || entities.Contains(departure.Subject)) continue;
|
||||
if (departure.Signature * PassiveInfo(departure.Position, 1f) > threshold) EndReveals(departure.Subject, null);
|
||||
}
|
||||
// What stays of a departed subject is memory: pruned once its freshest entry is as old as ContactOf stops
|
||||
// remembering it.
|
||||
var memory = ItemManager.GameplaySettings.BearingMemory;
|
||||
foreach (var pair in _reveals)
|
||||
{
|
||||
if (entities.Contains(pair.Key)) continue;
|
||||
var freshest = float.MinValue;
|
||||
foreach (var reveal in pair.Value.Values) freshest = Math.Max(freshest, reveal.Time);
|
||||
if (Zone.Time - freshest >= memory) _agedSubjects.Add(pair.Key);
|
||||
}
|
||||
for (var i = 0; i < _agedSubjects.Count; i++) EndReveals(_agedSubjects[i], null);
|
||||
_agedSubjects.Clear();
|
||||
}
|
||||
|
||||
public Entity(ItemManager itemManager, Zone zone, EquippableItem hull, EntitySettings settings)
|
||||
|
|
@ -854,10 +1018,10 @@ public abstract class Entity
|
|||
for(int i=0; i<itemManager.GameplaySettings.WeaponGroupCount; i++)
|
||||
WeaponGroups[i] = (new List<Weapon>(), new List<EquippedItem>());
|
||||
|
||||
ItemDestroyed = ItemDamage.Where(x => x.item.EquippableItem.Durability < .01f).Select(x=>x.item);
|
||||
ItemDestroyed = ItemDamage.Where(x => x.item.EquippableItem.IsDestroyed).Select(x=>x.item);
|
||||
WeaponDestroyed = ItemDestroyed.Select(x => x.Behaviors.FirstOrDefault(b => b is Weapon) as Weapon).Where(x => x != null);
|
||||
HullArmorDepleted = ArmorDamage.Where(x => Armor[x.pos.x, x.pos.y] < .01f).Select(x => x.pos);
|
||||
Death = HullDamage.Where(_ => Hull.Durability < .01f).Select(_ => CauseOfDeath.HullDestroyed)
|
||||
Death = HullDamage.Where(_ => Hull.IsDestroyed).Select(_ => CauseOfDeath.HullDestroyed)
|
||||
.Merge(HeatstrokeDeath.Select(_ => CauseOfDeath.Heatstroke))
|
||||
.Merge(HypothermiaDeath.Select(_ => CauseOfDeath.Hypothermia))
|
||||
.Merge(ItemDestroyed.Where(i=>i.GetBehavior<Cockpit>()!=null).Select(_ => CauseOfDeath.CockpitDestroyed))
|
||||
|
|
@ -2035,7 +2199,7 @@ public class EquippedItem : IStatContext
|
|||
deltaTemp * ItemManager.GameplaySettings.DeltaTempWearExponent
|
||||
) * Data.Durability / Data.ThermalResilience;
|
||||
ThermalOnline.Value = ThermalPerformance > performanceThreshold || Entity.OverrideShutdown && EquippableItem.OverrideShutdown;
|
||||
DurabilityOnline.Value = EquippableItem.Durability > .01f;
|
||||
DurabilityOnline.Value = !EquippableItem.IsDestroyed;
|
||||
oldTemperature = temp;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ public static class FireControl
|
|||
// failing its own tier -- ranked.IndexOf(item) then returns -1 for it (the same "no longer on this
|
||||
// target" path below), and the survivors' tiers close up over the gap instead of leaving one.
|
||||
var ranked = target.Equipment
|
||||
.Where(e => e.Data.HardpointType != HardpointType.Hull && e.EquippableItem.Durability >= .01f)
|
||||
.Where(e => e.Data.HardpointType != HardpointType.Hull && !e.EquippableItem.IsDestroyed)
|
||||
.OrderByDescending(e => e.Data.HardpointType != HardpointType.Tool)
|
||||
.ThenByDescending(e => e.Data.Shape.Coordinates.Length)
|
||||
.ToList();
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ public class EquippableItem : CraftedItemInstance
|
|||
[JsonProperty("durability"), Key(7)] public float Durability;
|
||||
[JsonProperty("override"), Key(8)] public bool OverrideShutdown;
|
||||
|
||||
// The one rule for a destroyed item: its wear is spent.
|
||||
[IgnoreMember] public bool IsDestroyed => Durability < .01f;
|
||||
|
||||
// Cut 5 (docs/stats-and-power-cut.md §1.3, §0b "A power tier"): a player choice, stored on the unit rather
|
||||
// than the design, so two units of the same design can be prioritized differently. PowerTiers.Unassigned
|
||||
// until the first equip seeds a behaviour-kind default (EquippedItem's constructor) or the player overrides
|
||||
|
|
|
|||
|
|
@ -73,3 +73,63 @@ public readonly struct PaintWarning
|
|||
Strength = strength;
|
||||
}
|
||||
}
|
||||
|
||||
// What an observer has seen `Item` do (ruling visibility-per-capability-reveal): the channel it showed on, the
|
||||
// bearing at that act and the zone clock then. Held per (subject, item) in the observer's Entity; the latest act
|
||||
// replaces the earlier one. Position is never carried: only the track gives it.
|
||||
public readonly struct Reveal
|
||||
{
|
||||
public readonly EquippedItem Item;
|
||||
public readonly SignatureChannel Channel;
|
||||
public readonly float2 Bearing;
|
||||
public readonly float Time;
|
||||
|
||||
public Reveal(EquippedItem item, SignatureChannel channel, float2 bearing, float time)
|
||||
{
|
||||
Item = item;
|
||||
Channel = channel;
|
||||
Bearing = bearing;
|
||||
Time = time;
|
||||
}
|
||||
}
|
||||
|
||||
// A subject's removal from a zone as the zone recorded it (death, dock, warp, any removal): where it was and how
|
||||
// bright its light was (its signature outside the Emission channel). Judged by each observer's Sense under the
|
||||
// light-act gate; notification only, one tick long, it decides nothing.
|
||||
public readonly struct Departure
|
||||
{
|
||||
public readonly Entity Subject;
|
||||
public readonly float2 Position;
|
||||
public readonly float Signature;
|
||||
|
||||
public Departure(Entity subject, float2 position, float signature)
|
||||
{
|
||||
Subject = subject;
|
||||
Position = position;
|
||||
Signature = signature;
|
||||
}
|
||||
}
|
||||
|
||||
// How much an observer holds of one subject, derived on read by Entity.ContactOf from its track, its reveals and
|
||||
// the zone clock. Detected: tracked, or a reveal younger than GameplaySettings.BearingMemory. Tracked: exactly
|
||||
// membership of VisibleEntities. Classified: detected, with any reveal in the book. Identified: the track has reached
|
||||
// TargetGearInfoThreshold. Bearing is the true bearing while tracked, else the freshest reveal's.
|
||||
public readonly struct Contact
|
||||
{
|
||||
public readonly bool Detected;
|
||||
public readonly bool Tracked;
|
||||
public readonly bool Classified;
|
||||
public readonly bool Identified;
|
||||
public readonly float2 Bearing;
|
||||
public readonly float Info;
|
||||
|
||||
public Contact(bool detected, bool tracked, bool classified, bool identified, float2 bearing, float info)
|
||||
{
|
||||
Detected = detected;
|
||||
Tracked = tracked;
|
||||
Classified = classified;
|
||||
Identified = identified;
|
||||
Bearing = bearing;
|
||||
Info = info;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -213,9 +213,12 @@ public class GameplaySettings
|
|||
public ExponentialLerp LockSpinSpeed;
|
||||
public float TorqueMultiplier;
|
||||
public float VisibilityDecay;
|
||||
// Seconds a reveal's bearing keeps a ship Detected after its last act with no track (Entity.ContactOf).
|
||||
public float BearingMemory = 6;
|
||||
public float TargetInfoDecay;
|
||||
public float TargetDetectionInfoThreshold;
|
||||
public float TargetArmorInfoThreshold;
|
||||
// Also the Identified threshold: the track level at which a ship's hull class, and with it its layout, is known.
|
||||
public float TargetGearInfoThreshold;
|
||||
public float ConvergenceMinimumDistance;
|
||||
public float FiringArc = 120;
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ public class Zone
|
|||
return false;
|
||||
}
|
||||
|
||||
private readonly List<Departure> _departures = new List<Departure>();
|
||||
internal IReadOnlyList<Departure> Departures => _departures;
|
||||
|
||||
public float Time
|
||||
{
|
||||
get => (float) _time;
|
||||
|
|
@ -109,6 +112,9 @@ public class Zone
|
|||
// every join, whichever caller admits the entity (deserialization below, a jump, a spawned turret) --
|
||||
// ObserveAdd fires for all of them. Forbidden writer: no presentation may remove an entity from Zone.
|
||||
Entities.ObserveAdd().Subscribe(add => add.Value.Death.Subscribe(_ => { Entities.Remove(add.Value); add.Value.Deactivate(); }));
|
||||
// The one record of a removal for observers (ruling reveal-ends-on-perception): read by the next Sense pass.
|
||||
Entities.ObserveRemove().Subscribe(remove => _departures.Add(new Departure(remove.Value, remove.Value.Position.xz,
|
||||
remove.Value.Visibility - remove.Value.SignatureOn(SignatureChannel.Emission))));
|
||||
|
||||
Targets.Add(new EntityTargets());
|
||||
|
||||
|
|
@ -229,6 +235,8 @@ public class Zone
|
|||
var entities = Entities.ToArray();
|
||||
foreach (var entity in entities) entity.PublishInbox();
|
||||
foreach (var entity in entities) entity.Update(deltaTime);
|
||||
foreach (var entity in entities) entity.Sense();
|
||||
_departures.Clear();
|
||||
|
||||
StepMines(deltaTime);
|
||||
StepClouds(deltaTime);
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ public class PropertiesPanel : MonoBehaviour
|
|||
|
||||
private void AddEquippableItemProperties(EquippableItem item, Func<PerformanceStat, float> statValueFunction)
|
||||
{
|
||||
if (item.Durability < .01f)
|
||||
if (item.IsDestroyed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -473,7 +473,7 @@ public class PropertiesPanel : MonoBehaviour
|
|||
|
||||
var gearData = GameManager.ItemManager.GetData(item.EquippableItem);
|
||||
var statusSheet = AddStatSheet();
|
||||
if (item.EquippableItem.Durability < .01f)
|
||||
if (item.EquippableItem.IsDestroyed)
|
||||
statusSheet.AddStat("Durability", () => "Item Destroyed!");
|
||||
else statusSheet.AddStat("Durability", () => $"{(int)(item.EquippableItem.Durability / gearData.Durability * 100)}%");
|
||||
statusSheet.AddStat("Temperature", () => ActionGameManager.PlayerSettings.FormatTemperature(item.Temperature));
|
||||
|
|
|
|||
1198
tests/Aetheria.Shared.Tests/ContactRevealTests.cs
Normal file
1198
tests/Aetheria.Shared.Tests/ContactRevealTests.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -313,6 +313,32 @@ public sealed partial class MiningCut3Tests : IDisposable
|
|||
$"per-tick loop settled at {perTick}, the on-demand value implies {discrete} (settled {settled})");
|
||||
}
|
||||
|
||||
// ChunkInfo's value is not changed by moving its arithmetic under PassiveInfo: six observers at different
|
||||
// places and bearings, one with a second sensor so the average is live, read the same chunk, and every value
|
||||
// matches the bits recorded at base 12dfcef5, before the extraction (a different order of the multiplication
|
||||
// by the chunk's brightness moves them in the last bits).
|
||||
private static readonly uint[] ChunkInfoAtBase = new uint[] { 1045120030u, 1039274086u, 1030543422u, 1029120970u, 1036363351u, 1035122917u };
|
||||
|
||||
[Fact]
|
||||
public void ChunkInfoIsBitIdenticalToItsValueBeforeTheExtraction()
|
||||
{
|
||||
var kind = Kind("Asteroid", 1f);
|
||||
var s = BuildScene(new[] { kind }, Belt(kind, Rock(150f)));
|
||||
var chunk = new ChunkId(s.Belts[0], 0);
|
||||
var places = new[] { float2(50, -150), float2(-80, -60), float2(10, 200), float2(-300, 40), float2(120, 120), float2(0, -400) };
|
||||
var values = new List<float>();
|
||||
for (var i = 0; i < places.Length; i++)
|
||||
{
|
||||
var observer = SpawnShip(s, places[i], sensor: true, secondSensor: i == 2);
|
||||
Tick((observer, places[i])); // brings the sensors online
|
||||
values.Add(observer.ChunkInfo(chunk));
|
||||
}
|
||||
|
||||
Assert.All(values, v => Assert.InRange(v, 1e-4f, .99f)); // a live, unsaturated reading at every place
|
||||
var actual = values.Select(Bits).ToArray();
|
||||
Assert.True(actual.SequenceEqual(ChunkInfoAtBase), "ChunkInfo bits: " + string.Join(",", actual));
|
||||
}
|
||||
|
||||
// A chunk outside every sun's light is dark: no visibility, no info, and it cannot be picked, while a lit
|
||||
// chunk of the same belt and kind can.
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Reference in a new issue