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:
parent
cb14a816f8
commit
76aeeabfca
18 changed files with 457 additions and 69 deletions
|
|
@ -36,18 +36,20 @@ public class CombatState : BaseState
|
|||
|
||||
// Cut 2 (docs/fire-control-cut.md): aim at whichever revealed weapon on the target hits hardest at
|
||||
// this range, or clear the aim point when nothing qualifies -- the same predicate
|
||||
// (FireControl.IsRevealed) and the same writer (TrySelectTargetItem) the player path uses.
|
||||
// (FireControl.IsRevealed) and the same writer (TrySelectTargetItem) the player path uses. Which weapons it
|
||||
// believes the target has, and how hard each hits, come from its book's claims, not the target's live gear
|
||||
// (aetheria-ew, classify-from-belief; ew-claim-weapon-measure-ruled: the claimed design's rated figures); the
|
||||
// reveal's Item is the aim and stays the truth.
|
||||
EquippedItem bestTargetItem = null;
|
||||
var bestTargetDps = float.MinValue;
|
||||
foreach (var targetItem in target.Equipment)
|
||||
foreach (var reveal in _agent.Ship.RevealedWeapons(target))
|
||||
{
|
||||
if (!(targetItem.Behaviors.FirstOrDefault(b => b is Weapon) is Weapon targetWeapon)) continue;
|
||||
if (!FireControl.IsRevealed(_agent.Ship, targetItem)) continue;
|
||||
var dps = targetWeapon.RangeDamagePerSecond(targetDistance);
|
||||
if (!FireControl.IsRevealed(_agent.Ship, reveal.Item)) continue;
|
||||
var dps = reveal.Claimed.Behaviors.OfType<WeaponData>().First().RatedRangeDamagePerSecond(targetDistance);
|
||||
if (dps > bestTargetDps)
|
||||
{
|
||||
bestTargetDps = dps;
|
||||
bestTargetItem = targetItem;
|
||||
bestTargetItem = reveal.Item;
|
||||
}
|
||||
}
|
||||
_agent.Ship.TrySelectTargetItem(bestTargetItem);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,15 @@ public class ChargedWeaponData : InstantWeaponData
|
|||
{
|
||||
return new ChargedWeapon(this, item);
|
||||
}
|
||||
|
||||
// The one formula of a charged weapon's damage per second at a range (see InstantWeaponData.RangeDamagePerSecond).
|
||||
internal static float RangeDamagePerSecond(BezierCurve damageCurve, float damage, float chargeFiringDamageMultiplier,
|
||||
float minRange, float range, float cooldown, float chargeTime, float atRange) =>
|
||||
damage * chargeFiringDamageMultiplier * damageCurve.Evaluate(saturate(unlerp(minRange, range, atRange))) / (cooldown + chargeTime);
|
||||
|
||||
public override float RatedRangeDamagePerSecond(float range) =>
|
||||
RangeDamagePerSecond(DamageCurve, Damage.Max / (int) Count.Max, ChargeFiringDamageMultiplier, MinRange.Max, Range.Max,
|
||||
Cooldown.Max, ChargeTime.Max, range);
|
||||
}
|
||||
|
||||
public class ChargedWeapon : InstantWeapon
|
||||
|
|
@ -78,10 +87,8 @@ public class ChargedWeapon : InstantWeapon
|
|||
public override float DamagePerSecond => Damage * _data.ChargeFiringDamageMultiplier / (Cooldown + ChargeTime);
|
||||
public override float RangeDamagePerSecond(float range)
|
||||
{
|
||||
return Damage *
|
||||
_data.ChargeFiringDamageMultiplier *
|
||||
_data.DamageCurve.Evaluate(saturate(unlerp(MinRange, Range, range))) /
|
||||
(Cooldown + ChargeTime);
|
||||
return ChargedWeaponData.RangeDamagePerSecond(_data.DamageCurve, Damage, _data.ChargeFiringDamageMultiplier,
|
||||
MinRange, Range, Cooldown, ChargeTime, range);
|
||||
}
|
||||
|
||||
public event Action OnStartCharging;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ public class ConstantWeaponData : WeaponData
|
|||
{
|
||||
return new ConstantWeapon(this, item);
|
||||
}
|
||||
|
||||
// The one formula of a beam's damage per second at a range (see InstantWeaponData.RangeDamagePerSecond).
|
||||
internal static float RangeDamagePerSecond(BezierCurve damageCurve, float damage, float minRange, float range, float atRange) =>
|
||||
damage * damageCurve.Evaluate(saturate(unlerp(minRange, range, atRange)));
|
||||
|
||||
public override float RatedRangeDamagePerSecond(float range) =>
|
||||
RangeDamagePerSecond(DamageCurve, Damage.Max, MinRange.Max, Range.Max, range);
|
||||
}
|
||||
|
||||
public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerConsumer
|
||||
|
|
@ -49,7 +56,7 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerC
|
|||
public override float DamagePerSecond => Damage;
|
||||
public override float RangeDamagePerSecond(float range)
|
||||
{
|
||||
return Damage * _data.DamageCurve.Evaluate(saturate(unlerp(MinRange, Range, range)));
|
||||
return ConstantWeaponData.RangeDamagePerSecond(_data.DamageCurve, Damage, MinRange, Range, range);
|
||||
}
|
||||
|
||||
public event Action OnReloadBegin;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,14 @@ public class InstantWeaponData : WeaponData
|
|||
{
|
||||
return new InstantWeapon(this, item);
|
||||
}
|
||||
|
||||
// The one formula of an instant weapon's damage per second at a range; the live weapon calls it with its evaluated
|
||||
// stats, the design with its rated ones (burst divides damage, as UpdateStats does).
|
||||
internal static float RangeDamagePerSecond(BezierCurve damageCurve, float damage, float minRange, float range, float cooldown, float atRange) =>
|
||||
damage * damageCurve.Evaluate(saturate(unlerp(minRange, range, atRange))) / cooldown;
|
||||
|
||||
public override float RatedRangeDamagePerSecond(float range) =>
|
||||
RangeDamagePerSecond(DamageCurve, Damage.Max / (int) Count.Max, MinRange.Max, Range.Max, Cooldown.Max, range);
|
||||
}
|
||||
|
||||
public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerConsumer
|
||||
|
|
@ -61,7 +69,7 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
public override float DamagePerSecond => Damage / Cooldown;
|
||||
public override float RangeDamagePerSecond(float range)
|
||||
{
|
||||
return Damage * _data.DamageCurve.Evaluate(saturate(unlerp(MinRange, Range, range))) / Cooldown;
|
||||
return InstantWeaponData.RangeDamagePerSecond(_data.DamageCurve, Damage, MinRange, Range, Cooldown, range);
|
||||
}
|
||||
|
||||
public override int Ammo
|
||||
|
|
|
|||
|
|
@ -71,6 +71,12 @@ public abstract class WeaponData : BehaviorData
|
|||
// weapon-tracking-authored). Unauthored is a mount that follows anything: FireControl.PMount reads +infinity as 1.
|
||||
[Inspectable, JsonProperty("tracking"), Key(33), RuntimeInspectable]
|
||||
public PerformanceStat Tracking = new PerformanceStat { Min = float.PositiveInfinity, Max = float.PositiveInfinity };
|
||||
|
||||
// What an observer who believes this is the design measures it by (aetheria-ew, ew-claim-weapon-measure-ruled): the
|
||||
// design's rated figures, each stat's Max (what a stat with no terms resolves to), through the same formula the
|
||||
// live weapon's RangeDamagePerSecond runs over its evaluated stats. Never the carrier's quality, heat, wear or power.
|
||||
public abstract float RatedRangeDamagePerSecond(float range);
|
||||
public float RatedRange => Range.Max;
|
||||
}
|
||||
|
||||
public abstract class Weapon : Behavior, IActivatedBehavior
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -20,15 +20,19 @@ public readonly struct Signature
|
|||
public readonly SignatureChannel Channel;
|
||||
public readonly float Strength;
|
||||
public readonly EquippedItem Item;
|
||||
// The class this act claims: the true item's Data unless its ship's EW claims otherwise (aetheria-ew,
|
||||
// classify-from-belief). Stamped by Entity.SetSignature from the ship's claim.
|
||||
public readonly EquippableItemData Claimed;
|
||||
|
||||
public Signature(SignatureChannel channel, float strength, EquippedItem item)
|
||||
public Signature(SignatureChannel channel, float strength, EquippedItem item, EquippableItemData claimed)
|
||||
{
|
||||
Channel = channel;
|
||||
Strength = strength;
|
||||
Item = item;
|
||||
Claimed = claimed;
|
||||
}
|
||||
|
||||
public Signature WithStrength(float strength) => new Signature(Channel, strength, Item);
|
||||
public Signature WithStrength(float strength) => new Signature(Channel, strength, Item, Claimed);
|
||||
}
|
||||
|
||||
// What a painted ship learns of a beam on it (ruling sensor-stat-set item 4): a Search paint is a ping sweeping
|
||||
|
|
@ -64,11 +68,14 @@ public readonly struct ActiveEmission
|
|||
{
|
||||
public readonly EquippedItem Item;
|
||||
public readonly float HearRange;
|
||||
// The class this emission claims (see Signature.Claimed); stamped by Entity.Emit.
|
||||
public readonly EquippableItemData Claimed;
|
||||
|
||||
public ActiveEmission(EquippedItem item, float hearRange)
|
||||
public ActiveEmission(EquippedItem item, float hearRange, EquippableItemData claimed)
|
||||
{
|
||||
Item = item;
|
||||
HearRange = hearRange;
|
||||
Claimed = claimed;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,17 +97,21 @@ public readonly struct PaintWarning
|
|||
|
||||
// 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.
|
||||
// replaces the earlier one. Position is never carried: only the track gives it. `Item` stays the true item: it is the
|
||||
// book's key and what aim reads. `Claimed` is the class the act claimed, and is what every classifying reader reads
|
||||
// (aetheria-ew, classify-from-belief).
|
||||
public readonly struct Reveal
|
||||
{
|
||||
public readonly EquippedItem Item;
|
||||
public readonly EquippableItemData Claimed;
|
||||
public readonly SignatureChannel Channel;
|
||||
public readonly float2 Bearing;
|
||||
public readonly float Time;
|
||||
|
||||
public Reveal(EquippedItem item, SignatureChannel channel, float2 bearing, float time)
|
||||
public Reveal(EquippedItem item, SignatureChannel channel, float2 bearing, float time, EquippableItemData claimed)
|
||||
{
|
||||
Item = item;
|
||||
Claimed = claimed;
|
||||
Channel = channel;
|
||||
Bearing = bearing;
|
||||
Time = time;
|
||||
|
|
@ -127,7 +138,9 @@ public readonly struct Departure
|
|||
// 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.
|
||||
// TargetGearInfoThreshold, and holds a hull belief: Profile, the hull class this observer believes the subject is (null
|
||||
// below Identified; aetheria-ew, classify-from-belief; seen-through arrives with the judge). Bearing is the true
|
||||
// bearing while tracked, else the freshest reveal's.
|
||||
public readonly struct Contact
|
||||
{
|
||||
public readonly bool Detected;
|
||||
|
|
@ -136,9 +149,11 @@ public readonly struct Contact
|
|||
public readonly bool Identified;
|
||||
public readonly float2 Bearing;
|
||||
public readonly float Info;
|
||||
public readonly HullData Profile;
|
||||
|
||||
public Contact(bool detected, bool tracked, bool classified, bool identified, float2 bearing, float info)
|
||||
public Contact(bool detected, bool tracked, bool classified, bool identified, float2 bearing, float info, HullData profile)
|
||||
{
|
||||
Profile = profile;
|
||||
Detected = detected;
|
||||
Tracked = tracked;
|
||||
Classified = classified;
|
||||
|
|
|
|||
Loading…
Reference in a new issue