Merge origin/master into generic-design-names

Conflicts: AetherDb usage line (kept master's list plus generic-designs [apply]), Program.cs class tail (kept the
launch-suppressor catalog and GenericDesigns both), GameData/Aetheria.cc taken from origin/master (f1642a12; the
generic-designs apply rerun follows in its own commit). The old-name sweep of the newly landed code follows separately.
This commit does not pass the generic-name tests: the catalog is master's, not yet renamed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-14 01:49:18 +02:00
commit a8394f3f92
41 changed files with 2106 additions and 381 deletions

View file

@ -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);

View file

@ -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, PerRound(Damage.Max, Count.Max), ChargeFiringDamageMultiplier, MinRange.Max, Range.Max,
Cooldown.Max, ChargeTime.Max, range);
}
public class ChargedWeapon : InstantWeapon
@ -75,13 +84,14 @@ public class ChargedWeapon : InstantWeapon
// authored charge-up speed IS its sustained rate of fire, not a second, disconnected number.
protected override float RateOverride => Evaluate(_data.ChargeEnergy);
public override float DamagePerSecond => Damage * _data.ChargeFiringDamageMultiplier / (Cooldown + ChargeTime);
// The charge multiplier lives in the formula alone: Damage is lerped by charge for the shot itself (UpdateStats), so the
// live reads take the uncharged damage, and a nominal weapon reads its design's rated figure at every charge.
private float _unchargedDamage;
public override float DamagePerSecond => _unchargedDamage * _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, _unchargedDamage, _data.ChargeFiringDamageMultiplier,
MinRange, Range, Cooldown, ChargeTime, range);
}
public event Action OnStartCharging;
@ -121,6 +131,7 @@ public class ChargedWeapon : InstantWeapon
ChargeTime = Evaluate(_data.ChargeTime);
ChargeEnergy = Evaluate(_data.ChargeEnergy);
ChargeHeat = Evaluate(_data.ChargeHeat);
_unchargedDamage = Damage;
Damage *= lerp(1, _data.ChargeFiringDamageMultiplier, saturate(_charge));
Heat *= lerp(1, _data.ChargeFiringHeatMultiplier, saturate(_charge));
Spread *= lerp(1, _data.ChargeFiringSpreadMultiplier, saturate(_charge));

View file

@ -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;

View file

@ -30,6 +30,17 @@ 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. A burst's round carries its share of the totals: PerRound, the one divisor
// UpdateStats and the rated read both use.
internal static float PerRound(float total, float burstCount) => total / (int) burstCount;
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, PerRound(Damage.Max, Count.Max), MinRange.Max, Range.Max, Cooldown.Max, range);
}
public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerConsumer
@ -61,7 +72,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
@ -129,9 +140,9 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
BurstTime = Evaluate(_data.BurstTime);
Cooldown = Evaluate(_data.Cooldown);
Damage /= (int) BurstCount;
Heat /= (int) BurstCount;
Energy /= (int) BurstCount;
Damage = InstantWeaponData.PerRound(Damage, BurstCount);
Heat = InstantWeaponData.PerRound(Heat, BurstCount);
Energy = InstantWeaponData.PerRound(Energy, BurstCount);
}
// Cut 4 (docs/stats-and-power-cut.md §7 Q4): capacity and rate, resolved fresh rather than off the cached

View file

@ -71,6 +71,13 @@ 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);
[IgnoreMember]
public float RatedRange => Range.Max;
}
public abstract class Weapon : Behavior, IActivatedBehavior

View file

@ -103,7 +103,27 @@ 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): per equipped item, the
// equippable class every act of that item is stamped with (SetSignature, Emit, ReceivePaint). The hull is the
// item EquippedHull, so its claim lives in the same slot as every other: the hull class its own acts carry and
// the profile an observer derives (ClaimedHull) are one value. 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 the slots. Aim, damage and physics keep the truth.
private readonly Dictionary<EquippedItem, EquippableItemData> _itemClaims = new Dictionary<EquippedItem, EquippableItemData>();
public HullData ClaimedHull => (ClaimOf(EquippedHull) as HullData) ?? HullData;
public EquippableItemData ClaimOf(EquippedItem item) =>
item == null ? null : _itemClaims.TryGetValue(item, out var claimed) ? claimed : item.Data;
internal void Claim(HullData hull) => Claim(EquippedHull, hull);
internal void Claim(EquippedItem item, EquippableItemData claimed)
{
// One hull class in the book: a hull class is claimed by the hull and only by the hull, whichever side asks.
if (claimed != null && (item == EquippedHull) != (claimed is HullData))
throw new ArgumentException("Only the hull claims a hull class, and the hull claims nothing else.", nameof(claimed));
if (claimed == null) _itemClaims.Remove(item);
else _itemClaims[item] = claimed;
}
public readonly ReactiveDictionary<Entity, float> EntityInfoGathered = new ReactiveDictionary<Entity, float>();
@ -868,6 +888,7 @@ public abstract class Entity
_pingReturns.Clear();
_emissions.Clear();
_reveals.Clear();
_itemClaims.Clear();
}
// Zone.Update runs this on every entity before any entity updates, so a paint made during tick N is published
@ -880,7 +901,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 +921,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 +939,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 +978,31 @@ 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 the
// subject's equipment order (never the book's slot order, which follows when reveals were made and ended). 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))
.OrderBy(r => FittedIndex(subject, r.Item)).ThenBy(r => r.Item.EquippableItem.Lot);
// A revealed item's place in the subject's equipment; gear the subject no longer carries follows all fitted gear
// (ties among it fall to the lot, a stable id), never ahead of it.
private static int FittedIndex(Entity subject, EquippedItem item)
{
var index = subject.Equipment.IndexOf(item);
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.
public IEnumerable<SchematicRow> 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));
// 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 +1024,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 +1050,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 +1061,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

View file

@ -0,0 +1,44 @@
using static CultMath.math;
using static ItemRotation;
public sealed class LockOn : Scenario
{
public override string Name => "Lock On";
public override string Brief => "A hostile closes from out of sight, finds you and locks. Verify: no warning at first; once it locks, one " +
"indicator appears toward it and tightens as the lock grows; break line of sight or range and it vanishes; " +
"your own ping shows no incoming paint on its side, only the lock does.";
public override uint Seed => 113;
public override bool Ambient => false;
// The hostile starts about 4000 m off, to the side of the player's axis, facing the player. At that range its passive
// sensors hold no track of the player (the test asks the sim's own PassiveInfo against the detection threshold); the
// player sees the hostile a little sooner, at about tick 20. The hostile closes, sees the player some eight seconds in,
// and locks two ticks later. At Duel's 900 m (and at 600 m) it sees and locks within a second.
public static readonly CultMath.float2 HostileAt = float2(1500, 3700);
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(DemoTerminus.Cast);
public override void Stage(ScenarioStage stage)
{
var fighter = stage.Fit("Longinus",
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("Core Power", int2(2, 4), None),
("GT 3K", int2(0, 5), None),
("GT 3K", int2(5, 5), None),
("FastBlast+-", int2(1, 8), None),
("FastBlast+-", int2(4, 8), None),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("PotaT+-", int2(1, 5), None),
("Fire Control Array", int2(1, 6), Clockwise),
("Store-All Plus", int2(2, 8), None));
stage.Player(fighter, float2(0, 0), facing: float2(0, 1));
stage.Place(fighter, HostileAt, facing: normalize(-HostileAt), stance: ScenarioStance.Hostile, piloted: true);
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3f0b6d2a9c7e4a1b8d5e2c6f7a9b0c14
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -9,6 +9,6 @@ public static class Scenarios
public static readonly Scenario[] Tests =
{
new DjinniShakedown(), new StarvedReactor(), new Arcs(), new Duel(), new LauncherAngles(), new WeaponFeel(), new FusedRounds(), new RefusedRounds(), new AiFusedDiscipline(), new LongHaul(), new Pursuit(), new Crossing(), new Belt()
new DjinniShakedown(), new StarvedReactor(), new Arcs(), new Duel(), new LauncherAngles(), new WeaponFeel(), new FusedRounds(), new RefusedRounds(), new AiFusedDiscipline(), new LongHaul(), new Pursuit(), new Crossing(), new Belt(), new LockOn()
};
}

View file

@ -2,6 +2,7 @@
* 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;
// Light signatures split by channel (ruling visibility-per-capability-reveal): an observer learns what a ship
@ -20,15 +21,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 +69,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,23 +98,53 @@ 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;
}
}
// One weapon 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.
public readonly struct SchematicRow
{
public readonly EquippedItem Item;
public readonly EquippableItemData Claimed;
private readonly bool _own;
internal SchematicRow(EquippedItem item, EquippableItemData claimed, bool own)
{
Item = item;
Claimed = claimed;
_own = own;
}
// 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
? Item.Behaviors.OfType<Weapon>().First().Range
: Claimed.Behaviors.OfType<WeaponData>().First().RatedRange;
}
// 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 Visibility at removal). Judged by each observer's Sense under the
// light-act gate; notification only, one tick long, it decides nothing.
@ -127,7 +165,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 +176,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;

View file

@ -394,6 +394,10 @@ public class Zone
// After StepClouds and before FireControl.Step: each floating item drifts as a KinematicBody on the zone's force
// and is removed once its lifetime has run out. A held item is neither integrated nor expired: it is placed
// between where it was claimed and its holder, now that the entities have moved.
// The curve a held item rides from where it was claimed to its holder: the one owner, which the test scene's
// stand-in sim and tendril call rather than spell again.
public static float HeldPull(float progress) => smoothstep(0f, 1f, progress);
private void StepFloatingItems(float deltaTime)
{
var settings = _itemManager.GameplaySettings;
@ -401,7 +405,7 @@ public class Zone
{
if (floating.Holder != null)
{
floating.Body.Position = lerp(floating.HeldFrom, floating.Holder.Position.xz, smoothstep(0f, 1f, floating.HeldProgress));
floating.Body.Position = lerp(floating.HeldFrom, floating.Holder.Position.xz, HeldPull(floating.HeldProgress));
continue;
}
floating.Body.Step(deltaTime, GetForce(floating.Body.Position), settings);