EW 2 fixes: one hull claim slot, explicit tie order, one burst divisor, fixtures that see range and live state

- Entity: delete _hullClaim; the hull's claim is the EquippedHull entry of _itemClaims, so ClaimOf(EquippedHull)
  and ClaimedHull read one slot (hull-claim-split). Claim(HullData) writes it; a non-hull claim of the hull throws.
- RevealedWeapons orders by the subject's equipment order, not dictionary slot order (tie-order-unpinned).
- InstantWeaponData.PerRound is the one burst divisor for UpdateStats and the rated read (burst-divisor-twice).
- Tests: falling-curve, Range-span, MinRange>0 fixtures and a charged weapon in the rated tests (rated-figure-range-blind);
  Combat ranks claims at the target's distance, ignores a worn target gun's live figure (live-state-unpinned),
  and takes ties in equipment order.

Not yet run; may not build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-13 19:55:18 +02:00
commit a782c7d5d2
6 changed files with 205 additions and 45 deletions

View file

@ -61,7 +61,7 @@ public class ChargedWeaponData : InstantWeaponData
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,
RangeDamagePerSecond(DamageCurve, PerRound(Damage.Max, Count.Max), ChargeFiringDamageMultiplier, MinRange.Max, Range.Max,
Cooldown.Max, ChargeTime.Max, range);
}

View file

@ -32,12 +32,15 @@ public class InstantWeaponData : WeaponData
}
// 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).
// 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, Damage.Max / (int) Count.Max, MinRange.Max, Range.Max, Cooldown.Max, range);
RangeDamagePerSecond(DamageCurve, PerRound(Damage.Max, Count.Max), MinRange.Max, Range.Max, Cooldown.Max, range);
}
public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerConsumer
@ -137,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

@ -106,18 +106,21 @@ public abstract class Entity
_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;
// 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 => _hullClaim ?? HullData;
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) => _hullClaim = hull;
internal void Claim(HullData hull) => Claim(EquippedHull, hull);
internal void Claim(EquippedItem item, EquippableItemData claimed)
{
if (item == EquippedHull && claimed != null && !(claimed is HullData))
throw new ArgumentException("The hull can only claim to be a hull.", nameof(claimed));
if (claimed == null) _itemClaims.Remove(item);
else _itemClaims[item] = claimed;
}
@ -884,7 +887,6 @@ public abstract class Entity
_pingReturns.Clear();
_emissions.Clear();
_reveals.Clear();
_hullClaim = null;
_itemClaims.Clear();
}
@ -975,11 +977,13 @@ 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.
// 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));
RevealsOf(subject).Where(r => r.Claimed != null && r.Claimed.Behaviors.Any(b => b is WeaponData))
.OrderBy(r => subject.Equipment.IndexOf(r.Item));
// Whether this observer's book for `subject` holds `item`: the book's membership, no live gear read.
internal bool HasReveal(Entity subject, EquippedItem item) =>