EW 2: claims ride the act into the book; schematic and AI classify from them

Signature, ActiveEmission and Reveal carry Claimed (the class the ship's EW claims for the item, the true item's data by default); Entity gains the claim slots and one write port; Contact gains Profile, derived from the ship's claimed hull once Identified; Entity.RevealedWeapons is the one classifying read for weapons. Combat and the enemy schematic read it; their reads of the target's live gear are cut. Weapons rate by their claimed design (ew-claim-weapon-measure-ruled, rated-design): WeaponData.RatedRangeDamagePerSecond runs the same per-kind formula as the live weapon. Aim, damage and physics keep the truth.

Not yet compiled or run at this commit; the Unity-only SchematicDisplay is unverified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-13 18:17:14 +02:00
commit 76aeeabfca
18 changed files with 457 additions and 69 deletions

View file

@ -103,7 +103,23 @@ public abstract class Entity
// each equipped suppressor folds the strength in equipment order and pays for what it alone removed.
foreach (var suppressor in GetBehaviors<LaunchSuppressor>())
strength = suppressor.Suppress(channel, strength);
_visibilitySources[source] = new Signature(channel, strength, item);
_visibilitySources[source] = new Signature(channel, strength, item, ClaimOf(item));
}
// What this ship's acts claim (aetheria-ew, lie-is-own-emission, classify-from-belief): the hull class and, per item,
// the equippable class every act of that item is stamped with (SetSignature, Emit, ReceivePaint). The truth unless
// claimed otherwise. Claim is the one write port; its production writer is the EW behaviour of cut ew-masquerade,
// tests only until then. Deactivate clears both. Aim, damage and physics keep the truth.
private HullData _hullClaim;
private readonly Dictionary<EquippedItem, EquippableItemData> _itemClaims = new Dictionary<EquippedItem, EquippableItemData>();
public HullData ClaimedHull => _hullClaim ?? HullData;
public EquippableItemData ClaimOf(EquippedItem item) =>
item == null ? null : _itemClaims.TryGetValue(item, out var claimed) ? claimed : item.Data;
internal void Claim(HullData hull) => _hullClaim = hull;
internal void Claim(EquippedItem item, EquippableItemData claimed)
{
if (claimed == null) _itemClaims.Remove(item);
else _itemClaims[item] = claimed;
}
public readonly ReactiveDictionary<Entity, float> EntityInfoGathered = new ReactiveDictionary<Entity, float>();
@ -868,6 +884,8 @@ public abstract class Entity
_pingReturns.Clear();
_emissions.Clear();
_reveals.Clear();
_hullClaim = null;
_itemClaims.Clear();
}
// Zone.Update runs this on every entity before any entity updates, so a paint made during tick N is published
@ -880,7 +898,7 @@ public abstract class Entity
foreach (var paint in _paintInbox)
{
_incomingPaints.Add(paint.Warning);
RecordReveal(paint.Emitter, paint.Item, SignatureChannel.Emission);
RecordReveal(paint.Emitter, paint.Item, SignatureChannel.Emission, paint.Warning.EmitterClass);
}
_paintInbox.Clear();
_pingReturns.Clear();
@ -900,16 +918,17 @@ public abstract class Entity
internal void Emit(EquippedItem item, float hearRange)
{
if (!_active) return;
_emissions.Add(new ActiveEmission(item, hearRange));
_emissions.Add(new ActiveEmission(item, hearRange, ClaimOf(item)));
}
// 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). 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).
// the emitter's own Lock at that paint). The emitter's class is the class its ship claims for the item (ClaimOf: the
// item's data unless claimed otherwise; 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 PendingPaint(new PaintWarning(normalize((emitter.Position - Position).xz), item?.Data, mode, strength), emitter, item));
_paintInbox.Add(new PendingPaint(new PaintWarning(normalize((emitter.Position - Position).xz), emitter.ClaimOf(item), mode, strength), emitter, item));
}
// The reveal book's one way in and its one membership gate (callers: Sense and PublishInbox, both in this file):
@ -917,13 +936,14 @@ public abstract class Entity
// 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)
// earlier one. `claimed` is the class the carrier (signature, emission or paint) stamped: what this observer's
// classifying readers will believe of the act; `item` stays the aim and book key.
internal void RecordReveal(Entity subject, EquippedItem item, SignatureChannel channel, EquippableItemData claimed)
{
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);
book[item] = new Reveal(item, channel, normalize((subject.Position - Position).xz), Zone.Time, claimed);
}
// This observer resolves `subject`'s gear (ContactOf's Identified gate) and `item` of it is destroyed: the one rule
@ -955,6 +975,12 @@ public abstract class Entity
public IEnumerable<Reveal> RevealsOf(Entity subject) =>
_reveals.TryGetValue(subject, out var book) ? book.Values : Enumerable.Empty<Reveal>();
// The weapons this observer believes `subject` has: each reveal whose claimed class carries a weapon behaviour, in book
// order. The one 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<Reveal> RevealedWeapons(Entity subject) =>
RevealsOf(subject).Where(r => r.Claimed != null && r.Claimed.Behaviors.Any(b => b is WeaponData));
// Whether this observer's book for `subject` holds `item`: the book's membership, no live gear read.
internal bool HasReveal(Entity subject, EquippedItem item) =>
_reveals.TryGetValue(subject, out var book) && book.ContainsKey(item);
@ -976,7 +1002,9 @@ public abstract class Entity
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);
var identified = info >= settings.TargetGearInfoThreshold;
// Profile is the one derivation of the hull this observer believes (no stored belief until the judge, ew-masquerade).
return new Contact(detected, tracked, detected && any, identified, bearing, info, identified ? subject.ClaimedHull : null);
}
// What this observer's instruments receive of other ships' acts this tick (Zone.Update calls it once, after every
@ -1000,7 +1028,7 @@ public abstract class Entity
{
if (signature.Item == null) continue;
if (perSignature < 0f) perSignature = PassiveInfo(other.Position.xz, 1f);
if (signature.Strength * perSignature > threshold) RecordReveal(other, signature.Item, signature.Channel);
if (signature.Strength * perSignature > threshold) RecordReveal(other, signature.Item, signature.Channel, signature.Claimed);
}
// Every emission this ship made this tick that reaches this observer is revealed, bearing only (ruling
// sensor-stat-set), through RecordReveal's one gate. It feeds no track.
@ -1011,7 +1039,7 @@ public abstract class Entity
for (var j = 0; j < other._emissions.Count; j++)
{
var emission = other._emissions[j];
if (planar <= emission.HearRange * hearing * obscuration) RecordReveal(other, emission.Item, SignatureChannel.Emission);
if (planar <= emission.HearRange * hearing * obscuration) RecordReveal(other, emission.Item, SignatureChannel.Emission, emission.Claimed);
}
}
// The observer resolves this ship's gear (PerceivesDestroyed), so it perceives an item of it destroyed and