Merge origin/master into loot-3 (second)
This commit is contained in:
commit
b836af4850
19 changed files with 857 additions and 82 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, 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));
|
||||
|
|
|
|||
|
|
@ -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,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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ public class SchematicDisplay : MonoBehaviour
|
|||
|
||||
public class SchematicDisplayItem
|
||||
{
|
||||
public EquippedItem Item;
|
||||
// The row Entity.SchematicRows derived: which item, which class and which range it shows.
|
||||
public SchematicRow Row;
|
||||
public SchematicListElement ListElement;
|
||||
public IProgressBehavior Cooldown;
|
||||
// public ItemUsage ItemUsage;
|
||||
|
|
@ -93,15 +94,12 @@ public class SchematicDisplay : MonoBehaviour
|
|||
CargoTemperatureLabel.text = "N/A";
|
||||
}
|
||||
|
||||
BuildRows(_enemy ? RevealedWeapons() : entity.Equipment.Where(IsWeapon));
|
||||
BuildRows(WeaponRows());
|
||||
}
|
||||
|
||||
private static bool IsWeapon(EquippedItem item) => item.Behaviors.Any(b => b.Data is WeaponData);
|
||||
|
||||
// An enemy schematic lists what `_player` has seen the ship's weapons do: the weapon items of the player's book on it,
|
||||
// in book order (ruling reveal-ends-on-perception), never the ship's live gear. The player's own schematic is its
|
||||
// equipment; there is nothing to reveal about your own ship.
|
||||
private IEnumerable<EquippedItem> RevealedWeapons() => _player.RevealsOf(_entity).Select(r => r.Item).Where(IsWeapon);
|
||||
// Which rows, which class and which range label are Entity.SchematicRows' decision (aetheria-ew, classify-from-belief);
|
||||
// this display only renders them.
|
||||
private IEnumerable<SchematicRow> WeaponRows() => (_player ?? _entity).SchematicRows(_entity);
|
||||
|
||||
private void ReturnRows()
|
||||
{
|
||||
|
|
@ -111,21 +109,21 @@ public class SchematicDisplay : MonoBehaviour
|
|||
_schematicItems = null;
|
||||
}
|
||||
|
||||
private void BuildRows(IEnumerable<EquippedItem> items)
|
||||
private void BuildRows(IEnumerable<SchematicRow> rows)
|
||||
{
|
||||
_schematicItems = items
|
||||
_schematicItems = rows
|
||||
.Select(x => new SchematicDisplayItem
|
||||
{
|
||||
Item = x,
|
||||
Row = x,
|
||||
ListElement = ListElementPrototype.Instantiate<SchematicListElement>(),
|
||||
Cooldown = _enemy ? null : (IProgressBehavior) x.Behaviors.FirstOrDefault(b=> b is IProgressBehavior),
|
||||
Cooldown = _enemy ? null : (IProgressBehavior) x.Item.Behaviors.FirstOrDefault(b=> b is IProgressBehavior),
|
||||
// ItemUsage = _enemy ? null : (ItemUsage) x.Behaviors.FirstOrDefault(b=> b is ItemUsage),
|
||||
Weapon = (Weapon) x.Behaviors.FirstOrDefault(b=>b is Weapon)
|
||||
Weapon = _enemy ? null : (Weapon) x.Item.Behaviors.FirstOrDefault(b=>b is Weapon)
|
||||
})
|
||||
.ToArray();
|
||||
foreach (var x in _schematicItems)
|
||||
{
|
||||
if(x.Item.Data is WeaponItemData weaponItemData)
|
||||
if(x.Row.Claimed is WeaponItemData weaponItemData)
|
||||
x.ListElement.ShowWeapon(weaponItemData);
|
||||
//x.ListElement.Label.text = x.Item.EquippableItem.Name;
|
||||
if (!_enemy)
|
||||
|
|
@ -197,10 +195,10 @@ public class SchematicDisplay : MonoBehaviour
|
|||
|
||||
// A reveal gained or ended after selection changes the rows with no event to hook: rebuild when the
|
||||
// revealed weapon set differs from the rows' items.
|
||||
if (_enemy && !RevealedWeapons().SequenceEqual(_schematicItems.Select(x => x.Item)))
|
||||
if (_enemy && !WeaponRows().Select(r => r.Key).SequenceEqual(_schematicItems.Select(x => x.Row.Key)))
|
||||
{
|
||||
ReturnRows();
|
||||
BuildRows(RevealedWeapons());
|
||||
BuildRows(WeaponRows());
|
||||
}
|
||||
|
||||
// Cut 2 (docs/fire-control-cut.md): marks whichever row is the player's current aim point.
|
||||
|
|
@ -208,7 +206,7 @@ public class SchematicDisplay : MonoBehaviour
|
|||
// as info decays, with no event to hook.
|
||||
if (_enemy && _schematicItems != null)
|
||||
foreach (var x in _schematicItems)
|
||||
x.ListElement.SetSelected(x.Item == _player.ResolvedTargetItem);
|
||||
x.ListElement.SetSelected(x.Row.Item == _player.ResolvedTargetItem);
|
||||
|
||||
if(VisibilityLabel && !_enemy)
|
||||
VisibilityLabel.text = ((int)_entity.Visibility).ToString();
|
||||
|
|
@ -226,11 +224,11 @@ public class SchematicDisplay : MonoBehaviour
|
|||
{
|
||||
if (!_enemy)
|
||||
{
|
||||
var itemData = _entity.ItemManager.GetData(x.Item.EquippableItem);
|
||||
x.ListElement.HeatFill.anchorMax = new Vector2(unlerp(itemData.MinimumTemperature, itemData.MaximumTemperature, x.Item.Temperature), 0);
|
||||
var itemData = _entity.ItemManager.GetData(x.Row.Item.EquippableItem);
|
||||
x.ListElement.HeatFill.anchorMax = new Vector2(unlerp(itemData.MinimumTemperature, itemData.MaximumTemperature, x.Row.Item.Temperature), 0);
|
||||
if (x.Cooldown != null)
|
||||
x.ListElement.CooldownFill.anchorMax = new Vector2(x.Cooldown.Progress, 1);
|
||||
x.ListElement.DurabilityLabel.text = $"{(int)(x.Item.EquippableItem.Durability / itemData.Durability * 100)}%";
|
||||
x.ListElement.DurabilityLabel.text = $"{(int)(x.Row.Item.EquippableItem.Durability / itemData.Durability * 100)}%";
|
||||
if (x.Weapon.WeaponData.AmmoType.IsSet())
|
||||
{
|
||||
if(x.Weapon.WeaponData.MagazineSize > 1)
|
||||
|
|
@ -240,11 +238,8 @@ public class SchematicDisplay : MonoBehaviour
|
|||
}
|
||||
}
|
||||
|
||||
if (x.Weapon.WeaponData != null)
|
||||
{
|
||||
if(x.ListElement.RangeLabel)
|
||||
x.ListElement.RangeLabel.text = ((int) x.Weapon.Range).ToString();
|
||||
}
|
||||
if (x.ListElement.RangeLabel)
|
||||
x.ListElement.RangeLabel.text = ((int) x.Row.Range).ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
464
tests/Aetheria.Shared.Tests/ClaimTests.cs
Normal file
464
tests/Aetheria.Shared.Tests/ClaimTests.cs
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
using float3 = CultMath.float3;
|
||||
|
||||
// Cut ew-claims-in-the-book (aetheria-ew, classify-from-belief, lie-is-own-emission, belief-one-writer): every act a ship
|
||||
// makes is stamped with the class its ship claims for the item (the true item's data unless claimed otherwise), the
|
||||
// observer's book keeps that claim beside the true item, and every classifying read takes the claim while aim keeps the
|
||||
// truth. The fixtures drive the production path: real ships fire, ping and lock inside Zone.Update.
|
||||
public sealed partial class ContactRevealTests
|
||||
{
|
||||
private EquippableItemData DataOf(string name) => (EquippableItemData) _items.ItemData.GetByName<ItemData>(name);
|
||||
|
||||
private static PerformanceStat Span(float min, float max) => new PerformanceStat { Min = min, Max = max };
|
||||
|
||||
// A damage curve that falls from 1 to .2 over the range span, so every range argument lands on a different factor.
|
||||
private static BezierCurve Falloff() => new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, .2f, 0, 0) } };
|
||||
|
||||
private static BezierCurve Flat() => new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } };
|
||||
|
||||
// ---- ClaimsDefaultToTheTruth ----
|
||||
|
||||
[Fact]
|
||||
public void ClaimsDefaultToTheTruth()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var gun = Guns(e)[0];
|
||||
Fire(e, gun);
|
||||
Assert.Same(gun.Data, Assert.Single(o.RevealsOf(e)).Claimed); // a fired gun claims its own design
|
||||
|
||||
// The hull belief: none below the gear threshold, the true hull at it (TargetGearInfoThreshold .8).
|
||||
o.EntityInfoGathered[e] = .79f;
|
||||
Assert.Null(o.ContactOf(e).Profile);
|
||||
o.EntityInfoGathered[e] = .8f;
|
||||
Assert.Same(e.HullData, o.ContactOf(e).Profile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AHeardPingDefaultsToTheTruth()
|
||||
{
|
||||
var p = Add(float3(0, 0, 0), new[] { "Shout" });
|
||||
var hearer = Add(float3(149, 0, 0), new[] { "Eye" }); // off the beam: heard, never painted
|
||||
PingAt(p);
|
||||
Assert.Same(SensorItemOf(p).Data, Assert.Single(hearer.RevealsOf(p)).Claimed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void APaintDefaultsToTheTruth()
|
||||
{
|
||||
var (locker, target) = LockOn("Locker");
|
||||
Tick(.5f);
|
||||
Tick(.5f);
|
||||
var lockItem = locker.Equipment.Single(i => i.Behaviors.Any(b => b is LockWeapon));
|
||||
Assert.Same(lockItem.Data, Assert.Single(target.IncomingPaints).EmitterClass);
|
||||
Assert.Same(lockItem.Data, Assert.Single(target.RevealsOf(locker)).Claimed);
|
||||
}
|
||||
|
||||
// ---- AnItemClaimIsWhatTheBookSays ----
|
||||
|
||||
// The book keeps the true item as its key and the claim beside it; RevealedWeapons lists it by the claim, and aim
|
||||
// still opens on the true item. Mutation: stamp item.Data in SetSignature, or have RecordReveal store the true data.
|
||||
[Fact]
|
||||
public void AnItemClaimIsWhatTheBookSays()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var gun = Guns(e)[0];
|
||||
var other = DataOf("Beam");
|
||||
e.Claim(gun, other);
|
||||
|
||||
Fire(e, gun);
|
||||
|
||||
var reveal = Assert.Single(o.RevealsOf(e));
|
||||
Assert.Same(gun, reveal.Item);
|
||||
Assert.Same(other, reveal.Claimed);
|
||||
Assert.Same(gun, Assert.Single(o.RevealedWeapons(e)).Item);
|
||||
Assert.True(FireControl.IsRevealed(o, gun));
|
||||
}
|
||||
|
||||
// ---- AGunClaimedAsNonWeaponIsNotListed ----
|
||||
|
||||
[Fact]
|
||||
public void AGunClaimedAsNonWeaponIsNotListed()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var gun = Guns(e)[0];
|
||||
e.Claim(gun, DataOf("Eye"));
|
||||
|
||||
Fire(e, gun);
|
||||
|
||||
Assert.Empty(o.RevealedWeapons(e));
|
||||
Assert.Single(o.RevealsOf(e)); // the book still holds the gun, for aim
|
||||
Assert.True(FireControl.IsRevealed(o, gun));
|
||||
}
|
||||
|
||||
// ---- AHullClaimIsTheProfile ----
|
||||
|
||||
// Mutation: ContactOf reads subject.HullData, or reads the claim below the gear threshold.
|
||||
[Fact]
|
||||
public void AHullClaimIsTheProfile()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var truth = e.HullData;
|
||||
var hull = e.Hull;
|
||||
var decoy = new HullData { Name = "Decoy" };
|
||||
e.Claim(decoy);
|
||||
Fire(e, Guns(e)[0]);
|
||||
|
||||
o.EntityInfoGathered[e] = .79f;
|
||||
Assert.Null(o.ContactOf(e).Profile);
|
||||
o.EntityInfoGathered[e] = .8f;
|
||||
Assert.Same(decoy, o.ContactOf(e).Profile);
|
||||
Assert.Same(truth, e.HullData); // the truth is untouched: damage, shape and physics keep it
|
||||
Assert.Same(hull, e.Hull);
|
||||
}
|
||||
|
||||
// ---- APaintCarriesTheClaim ----
|
||||
|
||||
[Fact]
|
||||
public void APaintCarriesTheClaim()
|
||||
{
|
||||
var locker = Add(float3(0, 0, 0), new[] { "Locker" });
|
||||
var target = Add(float3(0, 0, 200));
|
||||
var lockItem = locker.Equipment.Single(i => i.Behaviors.Any(b => b is LockWeapon));
|
||||
var claimed = DataOf("Eye");
|
||||
locker.Claim(lockItem, claimed);
|
||||
locker.SetIff(target, true);
|
||||
locker.EntityInfoGathered[target] = 1f;
|
||||
locker.SetTarget(target);
|
||||
locker.Aim = normalize(target.Position - locker.Position);
|
||||
|
||||
Tick(.5f);
|
||||
Tick(.5f);
|
||||
|
||||
Assert.Same(claimed, Assert.Single(target.IncomingPaints).EmitterClass);
|
||||
var reveal = Assert.Single(target.RevealsOf(locker));
|
||||
Assert.Same(lockItem, reveal.Item);
|
||||
Assert.Same(claimed, reveal.Claimed);
|
||||
}
|
||||
|
||||
// The emission a ping makes is stamped by the ship that makes it. Mutation: Emit stamps item.Data.
|
||||
[Fact]
|
||||
public void AHeardPingCarriesTheClaim()
|
||||
{
|
||||
var p = Add(float3(0, 0, 0), new[] { "Shout" });
|
||||
var hearer = Add(float3(149, 0, 0), new[] { "Eye" }); // off the beam: heard, never painted
|
||||
var claimed = DataOf("Ear");
|
||||
p.Claim(SensorItemOf(p), claimed);
|
||||
|
||||
PingAt(p);
|
||||
|
||||
Assert.Same(claimed, Assert.Single(hearer.RevealsOf(p)).Claimed);
|
||||
}
|
||||
|
||||
// ---- AClaimEndsWithItsShip ----
|
||||
|
||||
[Fact]
|
||||
public void AClaimEndsWithItsShip()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var gun = Guns(e)[0];
|
||||
e.Claim(new HullData { Name = "Decoy" });
|
||||
e.Claim(gun, DataOf("Eye"));
|
||||
|
||||
e.Deactivate();
|
||||
|
||||
Assert.Same(e.HullData, e.ClaimedHull);
|
||||
Assert.Same(gun.Data, e.ClaimOf(gun));
|
||||
}
|
||||
|
||||
// ---- Rated figures (ruling ew-claim-weapon-measure-ruled) ----
|
||||
|
||||
// A hull's claim is its own item's claim: the hull's acts (a Reflector's reflection, a launch flash, the hull as the
|
||||
// item of any signature) carry the class the profile reports, so the book never holds the true hull beside a decoy.
|
||||
// Mutation: ClaimedHull reads a slot of its own, or Claim(HullData) writes one.
|
||||
[Fact]
|
||||
public void AHullClaimReachesTheHullsOwnReveal()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var decoy = new HullData { Name = "Decoy" };
|
||||
e.Claim(decoy);
|
||||
Assert.Same(decoy, e.ClaimOf(e.EquippedHull));
|
||||
Assert.Same(decoy, e.ClaimedHull);
|
||||
|
||||
e.SetSignature(new object(), SignatureChannel.LaunchFlash, 1000000f, e.EquippedHull);
|
||||
Tick(.1f);
|
||||
|
||||
var reveal = Assert.Single(o.RevealsOf(e), r => r.Item == e.EquippedHull);
|
||||
Assert.Same(decoy, reveal.Claimed);
|
||||
o.EntityInfoGathered[e] = .8f;
|
||||
Assert.Same(reveal.Claimed, o.ContactOf(e).Profile);
|
||||
|
||||
e.Claim(null);
|
||||
Assert.Same(e.HullData, e.ClaimedHull);
|
||||
Assert.Same(e.HullData, e.ClaimOf(e.EquippedHull));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheHullCanOnlyClaimToBeAHull()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
Assert.Throws<System.ArgumentException>(() => e.Claim(e.EquippedHull, DataOf("Eye")));
|
||||
Assert.Same(e.HullData, e.ClaimedHull);
|
||||
}
|
||||
|
||||
// ---- Rated figures (ruling ew-claim-weapon-measure-ruled) ----
|
||||
|
||||
// A design's rated figure is each stat's Max through the live weapon's own formula. Every stat is authored with
|
||||
// Min below Max, a falling curve and a MinRange above 0, so reading a Min, scaling the range argument, the wrong
|
||||
// stat or the wrong operator changes the answer. At 70 the range factor is (70 - 20) / (120 - 20) = .5.
|
||||
// Mutation: any rated override reads Range.Min or MinRange.Min or a Min, drops the burst divisor, the charge
|
||||
// multiplier or the charge time; the shared formula scales its range argument.
|
||||
[Fact]
|
||||
public void ARatedFigureIsTheDesignsMaxThroughTheSharedFormula()
|
||||
{
|
||||
var curve = Falloff();
|
||||
var half = curve.Evaluate(.5f);
|
||||
Assert.NotEqual(half, curve.Evaluate(1f)); // the Range.Min and halved-range mutants land elsewhere on the curve
|
||||
Assert.NotEqual(half, curve.Evaluate(.15f));
|
||||
Assert.NotEqual(half, curve.Evaluate(70f / 120f));
|
||||
|
||||
var instant = Spanned(new InstantWeaponData());
|
||||
Assert.Equal(100f / 2 * half / 2, instant.RatedRangeDamagePerSecond(70f), 4); // 100 / 2 rounds, curve, / 2 s
|
||||
Assert.Equal(120f, instant.RatedRange);
|
||||
|
||||
var charged = (ChargedWeaponData) Spanned(new ChargedWeaponData
|
||||
{
|
||||
ChargeTime = Span(1, 3), ChargeFiringDamageMultiplier = 2f
|
||||
});
|
||||
Assert.Equal(100f / 2 * 2 * half / (2 + 3), charged.RatedRangeDamagePerSecond(70f), 4);
|
||||
|
||||
var beam = Spanned(new ConstantWeaponData());
|
||||
Assert.Equal(100f * half, beam.RatedRangeDamagePerSecond(70f), 4);
|
||||
Assert.Equal(120f, beam.RatedRange);
|
||||
}
|
||||
|
||||
// The rated read and the live read are one formula: a nominal weapon's live figure is its design's rated figure,
|
||||
// for each kind, at ranges before, inside and past the falloff span, and equals the hand arithmetic.
|
||||
// Mutation: a rated override with its own arithmetic, or a live read that drifts from the shared formula (the
|
||||
// charged read's ChargeTime, the range, the curve).
|
||||
[Fact]
|
||||
public void ARatedFigureIsTheLiveFigureOfANominalWeapon()
|
||||
{
|
||||
var e = Add(float3(0, 0, 0), new[] { "SpanGun", "SpanCharged", "SpanBeam" });
|
||||
Tick(.1f);
|
||||
var curve = Falloff();
|
||||
var expected = new Dictionary<string, Func<float, float>>
|
||||
{
|
||||
["SpanGun"] = at => 100f / 2 * curve.Evaluate(Math.Clamp((at - 20f) / 100f, 0f, 1f)) / 2,
|
||||
["SpanCharged"] = at => 100f / 2 * 2 * curve.Evaluate(Math.Clamp((at - 20f) / 100f, 0f, 1f)) / (2 + 3),
|
||||
["SpanBeam"] = at => 100f * curve.Evaluate(Math.Clamp((at - 20f) / 100f, 0f, 1f))
|
||||
};
|
||||
foreach (var item in e.Equipment.Where(i => i.Behaviors.Any(b => b is Weapon)))
|
||||
{
|
||||
var live = item.Behaviors.OfType<Weapon>().Single();
|
||||
var rated = item.Data.Behaviors.OfType<WeaponData>().Single();
|
||||
foreach (var at in new[] { 10f, 70f, 110f, 500f })
|
||||
{
|
||||
Assert.Equal(expected[item.Data.Name](at), live.RangeDamagePerSecond(at), 3);
|
||||
Assert.Equal(live.RangeDamagePerSecond(at), rated.RatedRangeDamagePerSecond(at), 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
// A charged weapon at full charge reads the same rated figure: the charge multiplier is applied once, in the formula
|
||||
// (UpdateStats lerps the shot's damage by charge, so the live reads start from the uncharged damage). At range 10 the
|
||||
// curve factor is 1: 100 / 2 rounds x 2 / (2 + 3) = 20. Mutation: the live reads pass the charge-lerped Damage.
|
||||
[Fact]
|
||||
public void AFullyChargedWeaponReadsItsRatedFigure()
|
||||
{
|
||||
var e = Add(float3(0, 0, 0), new[] { "SpanCharged" });
|
||||
Tick(.1f);
|
||||
var item = e.Equipment.Single(i => i.Data.Name == "SpanCharged");
|
||||
var live = (ChargedWeapon) item.Behaviors.Single(b => b is ChargedWeapon);
|
||||
var rated = item.Data.Behaviors.OfType<WeaponData>().Single();
|
||||
typeof(ChargedWeapon).GetField("_charge", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(live, 1f);
|
||||
typeof(Weapon).GetMethod("UpdateStats", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(live, null);
|
||||
|
||||
foreach (var at in new[] { 10f, 70f, 110f })
|
||||
Assert.Equal(rated.RatedRangeDamagePerSecond(at), live.RangeDamagePerSecond(at), 3);
|
||||
Assert.Equal(20f, live.DamagePerSecond, 3);
|
||||
Assert.Equal(20f, live.RangeDamagePerSecond(10f), 3);
|
||||
}
|
||||
|
||||
// ---- The schematic's rows (ruling ew-claim-weapon-measure-ruled; finding schematic-unreached) ----
|
||||
|
||||
// Wears a gun's Range to 95% (a shorter one is under the fuse arming distance and the gun is refused) so that, once it ticks, its live range (38) is not its design's rated range (40).
|
||||
private void WearRange(Entity ship, EquippedItem gun)
|
||||
{
|
||||
var data = (InstantWeaponData) gun.Data.Behaviors.Single(b => b is InstantWeaponData);
|
||||
ship.Resolver.AttachModifier(gun, data.Range, "worn", StatModifierType.Multiplier, .95f);
|
||||
}
|
||||
|
||||
// The fixture's guard, read once the gun has ticked: its live range is not its design's rated range.
|
||||
private static void AssertWorn(EquippedItem gun) =>
|
||||
Assert.Equal(38f, gun.Behaviors.OfType<Weapon>().Single().Range, 2);
|
||||
|
||||
// The unclaimed enemy row is the true gun, with the gun's own rated range (Gun: 40) though the gun is worn to a live
|
||||
// 38. Mutation: the row's Item.Data or the true live range stands in for the claim (also when Claimed == Data).
|
||||
[Fact]
|
||||
public void AnEnemySchematicRowDefaultsToTheTruth()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var gun = Guns(e)[0];
|
||||
WearRange(e, gun);
|
||||
Fire(e, gun);
|
||||
AssertWorn(gun);
|
||||
|
||||
var row = Assert.Single(o.SchematicRows(e));
|
||||
Assert.Same(gun, row.Item);
|
||||
Assert.Same(gun.Data, row.Claimed);
|
||||
Assert.Equal(40f, row.Range);
|
||||
Assert.NotEqual(gun.Behaviors.OfType<Weapon>().Single().Range, row.Range);
|
||||
}
|
||||
|
||||
// A weapon claimed as another weapon shows the claimed design's rated range (SpanGun: Range.Max 120), not the true
|
||||
// gun's 40. Mutation: Range reads the true item, or the claimed stat's Min (50), or scales the figure.
|
||||
[Fact]
|
||||
public void AWeaponClaimedAsAnotherWeaponShowsTheClaimedRatedRange()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var gun = Guns(e)[0];
|
||||
var other = DataOf("SpanGun");
|
||||
e.Claim(gun, other);
|
||||
Fire(e, gun);
|
||||
|
||||
var row = Assert.Single(o.SchematicRows(e));
|
||||
Assert.Same(gun, row.Item);
|
||||
Assert.Same(other, row.Claimed);
|
||||
Assert.Equal(120f, row.Range);
|
||||
}
|
||||
|
||||
// Mutation: the rows are the book's reveals (RevealsOf) or the subject's equipment.
|
||||
[Fact]
|
||||
public void AWeaponClaimedAsANonWeaponHasNoSchematicRow()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var gun = Guns(e)[0];
|
||||
e.Claim(gun, DataOf("Eye"));
|
||||
Fire(e, gun);
|
||||
|
||||
Assert.Empty(o.SchematicRows(e));
|
||||
Assert.Single(o.RevealsOf(e));
|
||||
}
|
||||
|
||||
// Unrevealed gear never appears on an enemy schematic: a ship that has not fired shows no row.
|
||||
[Fact]
|
||||
public void AnArmedShipThatNeverFiredHasNoEnemySchematicRow()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
Tick(.1f);
|
||||
Assert.Empty(o.SchematicRows(e));
|
||||
}
|
||||
|
||||
// Your own schematic is your own gear whatever you claim: the true data, the live range, weapons only.
|
||||
// Mutation: own rows read ClaimOf, the rated range of the claim (SpanGun 120) or of the own design (40), or list non-weapons.
|
||||
[Fact]
|
||||
public void OwnSchematicRowsShowOwnTruth()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var gun = Guns(e)[0];
|
||||
WearRange(e, gun);
|
||||
e.Claim(gun, DataOf("SpanGun"));
|
||||
Tick(.1f);
|
||||
AssertWorn(gun);
|
||||
|
||||
var row = Assert.Single(e.SchematicRows(e));
|
||||
Assert.Same(gun, row.Item);
|
||||
Assert.Same(gun.Data, row.Claimed);
|
||||
Assert.Equal(38f, row.Range, 2); // the live range of the worn gun, below its rated 40
|
||||
Assert.Equal(gun.Behaviors.OfType<Weapon>().Single().Range, row.Range);
|
||||
Assert.NotEqual(((WeaponData) gun.Data.Behaviors.Single(b => b is WeaponData)).RatedRange, row.Range);
|
||||
}
|
||||
|
||||
// The renderer rebuilds when the key differs: a claim that changes between two acts changes the key, and an
|
||||
// unchanged claim does not. Mutation: Key drops Claimed.
|
||||
[Fact]
|
||||
public void TheRebuildKeyChangesWhenAClaimChanges()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var gun = Guns(e)[0];
|
||||
Fire(e, gun);
|
||||
var before = o.SchematicRows(e).Select(r => r.Key).ToArray();
|
||||
Assert.Equal(before, o.SchematicRows(e).Select(r => r.Key).ToArray());
|
||||
|
||||
e.Claim(gun, DataOf("SpanGun"));
|
||||
e.SetSignature(new object(), SignatureChannel.LaunchFlash, 1000000f, gun);
|
||||
Tick(.1f);
|
||||
|
||||
var after = o.SchematicRows(e).Select(r => r.Key).ToArray();
|
||||
Assert.Single(after);
|
||||
Assert.NotEqual(before, after);
|
||||
Assert.Same(DataOf("SpanGun"), after[0].Item2);
|
||||
}
|
||||
|
||||
// Two guns of one claimed design are told apart by the key: a row set that swaps one gun for the other keeps the count and
|
||||
// the claims and changes only the item, and the renderer must still rebuild (the selection mark follows the item).
|
||||
// Mutation: Key drops Item.
|
||||
[Fact]
|
||||
public void TheRebuildKeyChangesWhenOnlyTheItemChanges()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun", "Gun" });
|
||||
var a = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var b = Add(float3(0, 0, 5), new[] { "Eye" });
|
||||
var guns = Guns(e);
|
||||
var design = DataOf("SpanGun");
|
||||
e.Claim(guns[0], design);
|
||||
e.Claim(guns[1], design);
|
||||
a.RecordReveal(e, guns[0], SignatureChannel.LaunchFlash, e.ClaimOf(guns[0]));
|
||||
b.RecordReveal(e, guns[1], SignatureChannel.LaunchFlash, e.ClaimOf(guns[1]));
|
||||
|
||||
var first = Assert.Single(a.SchematicRows(e));
|
||||
var second = Assert.Single(b.SchematicRows(e));
|
||||
Assert.Same(first.Claimed, second.Claimed);
|
||||
Assert.NotEqual(first.Key, second.Key);
|
||||
}
|
||||
|
||||
// ---- One hull class in the book ----
|
||||
|
||||
// The guard runs both ways: a hull class can be claimed only by the hull, so no item's reveal carries a hull class that
|
||||
// disagrees with Contact.Profile. Mutation: the guard checks the hull side only.
|
||||
[Fact]
|
||||
public void OnlyTheHullClaimsAHullClass()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var gun = Guns(e)[0];
|
||||
Assert.Throws<System.ArgumentException>(() => e.Claim(gun, e.HullData));
|
||||
Assert.Same(gun.Data, e.ClaimOf(gun));
|
||||
}
|
||||
|
||||
// ---- Gear the subject no longer carries ----
|
||||
|
||||
// A reveal of gear the subject does not carry follows every fitted weapon, and stale gear is ordered by lot whatever
|
||||
// order the observer saw it in. Mutation: the missing index sorts first (-1), or stale ties follow the book's slot order.
|
||||
[Fact]
|
||||
public void RevealsOfGearNoLongerCarriedFollowFittedGear()
|
||||
{
|
||||
var e = Add(float3(30, 0, 40), new[] { "Gun" });
|
||||
var gone = Add(float3(60, 0, 40), new[] { "Gun", "Gun" });
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var fitted = Guns(e)[0];
|
||||
var stale = Guns(gone);
|
||||
Assert.True(stale[0].EquippableItem.Lot < stale[1].EquippableItem.Lot);
|
||||
|
||||
o.RecordReveal(e, stale[1], SignatureChannel.LaunchFlash, stale[1].Data);
|
||||
o.RecordReveal(e, stale[0], SignatureChannel.LaunchFlash, stale[0].Data);
|
||||
o.RecordReveal(e, fitted, SignatureChannel.LaunchFlash, fitted.Data);
|
||||
|
||||
Assert.Equal(new[] { fitted, stale[0], stale[1] }, o.RevealedWeapons(e).Select(r => r.Item).ToArray());
|
||||
}
|
||||
}
|
||||
|
|
@ -89,6 +89,14 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
Fuse = WeaponFuse.Proximity, BlastRadius = 30f,
|
||||
Behaviors = { Armed(new ConstantWeaponData(), 0) }
|
||||
});
|
||||
// Weapons authored so the rated figure is a function of every stat: a falling damage curve, a Range span, a
|
||||
// MinRange above 0, burst and charge spans (a stat with no terms resolves to its Max live).
|
||||
cache.Upsert(SpannedWeapon("SpanGun", Spanned(new InstantWeaponData { BurstTime = Const(0) })));
|
||||
cache.Upsert(SpannedWeapon("SpanCharged", Spanned(new ChargedWeaponData
|
||||
{
|
||||
BurstTime = Const(0), ChargeTime = Span(1, 3), ChargeEnergy = Const(1), ChargeHeat = Const(0), ChargeFiringDamageMultiplier = 2f
|
||||
})));
|
||||
cache.Upsert(SpannedWeapon("SpanBeam", Spanned(new ConstantWeaponData())));
|
||||
cache.Upsert(Locker("Locker", interceptFactor: null));
|
||||
cache.Upsert(Locker("Hushed", interceptFactor: 4f));
|
||||
cache.Upsert(new ConsumableItemData
|
||||
|
|
@ -153,6 +161,29 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
return weapon;
|
||||
}
|
||||
|
||||
private static WeaponData Spanned(WeaponData weapon)
|
||||
{
|
||||
Armed(weapon, 5);
|
||||
weapon.Damage = Span(10, 100);
|
||||
weapon.MinRange = Span(0, 20);
|
||||
weapon.Range = Span(50, 120);
|
||||
weapon.DamageCurve = Falloff();
|
||||
if (weapon is InstantWeaponData instant)
|
||||
{
|
||||
instant.Count = Span(1, 2);
|
||||
instant.Cooldown = Span(8, 2);
|
||||
}
|
||||
return weapon;
|
||||
}
|
||||
|
||||
private static WeaponItemData SpannedWeapon(string name, WeaponData weapon) => new WeaponItemData
|
||||
{
|
||||
Name = name, Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 1,
|
||||
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
|
||||
Fuse = WeaponFuse.Proximity, BlastRadius = 30f,
|
||||
Behaviors = { weapon }
|
||||
};
|
||||
|
||||
private static GearData Suppressor(string name, float factor, float heatPerUnit)
|
||||
{
|
||||
var cell = new Shape(1, 1);
|
||||
|
|
|
|||
|
|
@ -478,7 +478,7 @@ public sealed class FireAuthorityTests : IDisposable
|
|||
var aimedItem = e.Target.Equipment.Single(x => x.EquippableItem == aimedGear);
|
||||
|
||||
e.Shooter.EntityInfoGathered[e.Target] = 1f; // fully tracked
|
||||
e.Shooter.RecordReveal(e.Target, aimedItem, SignatureChannel.LaunchFlash);
|
||||
e.Shooter.RecordReveal(e.Target, aimedItem, SignatureChannel.LaunchFlash, aimedItem.Data);
|
||||
Assert.True(e.Shooter.TrySelectTargetItem(aimedItem));
|
||||
|
||||
FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
|
||||
|
|
|
|||
|
|
@ -519,7 +519,7 @@ public sealed class FireControlCut12Tests : IDisposable
|
|||
var aim = new int2(2, 2);
|
||||
var e = Build(TestSettings(), SolidShape(6, 6), precision: 1000f, markerCells: new[] { aim });
|
||||
var aimed = e.Markers[0];
|
||||
e.Shooter.RecordReveal(e.Target, aimed, SignatureChannel.LaunchFlash);
|
||||
e.Shooter.RecordReveal(e.Target, aimed, SignatureChannel.LaunchFlash, aimed.Data);
|
||||
Assert.True(e.Shooter.TrySelectTargetItem(aimed));
|
||||
|
||||
// The aimed item is still equipped (Entity.TryUnequip refuses on an active entity, and reveal/aim
|
||||
|
|
@ -546,7 +546,7 @@ public sealed class FireControlCut12Tests : IDisposable
|
|||
public void AimingAtAnItemBiggerThanOneCellCentresOnItsCentroid()
|
||||
{
|
||||
var e = Build(TestSettings(), SolidShape(9, 9), precision: 1f, targetFacing: float2(1, 0), equipBigMarker: true);
|
||||
e.Shooter.RecordReveal(e.Target, e.BigMarker, SignatureChannel.LaunchFlash);
|
||||
e.Shooter.RecordReveal(e.Target, e.BigMarker, SignatureChannel.LaunchFlash, e.BigMarker.Data);
|
||||
Assert.True(e.Shooter.TrySelectTargetItem(e.BigMarker));
|
||||
|
||||
var travelDirection = FireControl.TravelDirection(e.Weapon, e.Shooter, e.Target);
|
||||
|
|
@ -584,7 +584,7 @@ public sealed class FireControlCut12Tests : IDisposable
|
|||
// the bounding-box centre along the diagonal, not along either grid axis -- fully onto the lateral
|
||||
// axis, instead of losing most of it to the axis a purely axis-aligned facing would leave unmeasured.
|
||||
var e = Build(TestSettings(), SolidShape(9, 9), precision: 1f, targetFacing: normalize(float2(1, 1)), equipLMarker: true);
|
||||
e.Shooter.RecordReveal(e.Target, e.LMarker, SignatureChannel.LaunchFlash);
|
||||
e.Shooter.RecordReveal(e.Target, e.LMarker, SignatureChannel.LaunchFlash, e.LMarker.Data);
|
||||
Assert.True(e.Shooter.TrySelectTargetItem(e.LMarker));
|
||||
|
||||
var travelDirection = FireControl.TravelDirection(e.Weapon, e.Shooter, e.Target);
|
||||
|
|
@ -975,7 +975,7 @@ public sealed class FireControlCut12Tests : IDisposable
|
|||
var aim = new int2(1, 1);
|
||||
var e = Build(settings, hullShape, precision, spread: 0, targetFacing: normalize(float2(1f, 1f)),
|
||||
markerCells: new[] { aim });
|
||||
e.Shooter.RecordReveal(e.Target, e.Markers[0], SignatureChannel.LaunchFlash);
|
||||
e.Shooter.RecordReveal(e.Target, e.Markers[0], SignatureChannel.LaunchFlash, e.Markers[0].Data);
|
||||
Assert.True(e.Shooter.TrySelectTargetItem(e.Markers[0]));
|
||||
|
||||
var shotId = FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
|
||||
|
|
@ -1158,7 +1158,7 @@ public sealed class FireControlCut12Tests : IDisposable
|
|||
|
||||
var aim = new int2(1, 1);
|
||||
var e = Build(TestSettings(), shape, precision: .3f, targetFacing: normalize(float2(.8f, .6f)), markerCells: new[] { aim });
|
||||
e.Shooter.RecordReveal(e.Target, e.Markers[0], SignatureChannel.LaunchFlash);
|
||||
e.Shooter.RecordReveal(e.Target, e.Markers[0], SignatureChannel.LaunchFlash, e.Markers[0].Data);
|
||||
Assert.True(e.Shooter.TrySelectTargetItem(e.Markers[0]));
|
||||
|
||||
var outcomes = FireMany(e, 1500);
|
||||
|
|
@ -1192,7 +1192,7 @@ public sealed class FireControlCut12Tests : IDisposable
|
|||
{
|
||||
var e = Build(settings, hullShape, precision: .6f, targetFacing: facing, markerCells: new[] { aimCell });
|
||||
var aimed = e.Markers[0];
|
||||
e.Shooter.RecordReveal(e.Target, aimed, SignatureChannel.LaunchFlash);
|
||||
e.Shooter.RecordReveal(e.Target, aimed, SignatureChannel.LaunchFlash, aimed.Data);
|
||||
Assert.True(e.Shooter.TrySelectTargetItem(aimed));
|
||||
Assert.Same(aimed, e.Shooter.ResolvedTargetItem);
|
||||
|
||||
|
|
|
|||
|
|
@ -587,8 +587,8 @@ public sealed class FireControlCut5Tests : IDisposable
|
|||
target.Zone.Admit(observer, piloted: false);
|
||||
observer.SetTarget(target);
|
||||
|
||||
observer.RecordReveal(target, big, SignatureChannel.LaunchFlash);
|
||||
observer.RecordReveal(target, medium, SignatureChannel.LaunchFlash);
|
||||
observer.RecordReveal(target, big, SignatureChannel.LaunchFlash, big.Data);
|
||||
observer.RecordReveal(target, medium, SignatureChannel.LaunchFlash, medium.Data);
|
||||
Assert.True(FireControl.IsRevealed(observer, big));
|
||||
Assert.True(FireControl.IsRevealed(observer, medium));
|
||||
Assert.True(observer.TrySelectTargetItem(big));
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ public sealed class FireControlCut6dTests : IDisposable
|
|||
if (marker != null)
|
||||
{
|
||||
var markerItem = target.Equipment.Single(x => x.EquippableItem == marker);
|
||||
shooter.RecordReveal(target, markerItem, SignatureChannel.LaunchFlash);
|
||||
shooter.RecordReveal(target, markerItem, SignatureChannel.LaunchFlash, markerItem.Data);
|
||||
Assert.True(shooter.TrySelectTargetItem(markerItem));
|
||||
Assert.Same(markerItem, shooter.ResolvedTargetItem);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ public sealed class FireControlCut7Tests : IDisposable
|
|||
|
||||
// The shooter tracks the target fully and has seen the interior item act; it selects it as the aim point.
|
||||
e.Shooter.EntityInfoGathered[e.Target] = 1f;
|
||||
e.Shooter.RecordReveal(e.Target, aimedItem, SignatureChannel.LaunchFlash);
|
||||
e.Shooter.RecordReveal(e.Target, aimedItem, SignatureChannel.LaunchFlash, aimedItem.Data);
|
||||
Assert.True(e.Shooter.TrySelectTargetItem(aimedItem));
|
||||
Assert.Same(aimedItem, e.Shooter.ResolvedTargetItem);
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,13 @@ public sealed class FireControlTests : IDisposable
|
|||
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
|
||||
Behaviors = { new InstantWeaponData() }
|
||||
});
|
||||
// A second design, so a target can carry two guns whose true figures differ (claim tests).
|
||||
buildCache.Upsert(new GearData
|
||||
{
|
||||
Name = "WeakGun", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 1,
|
||||
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
|
||||
Behaviors = { new InstantWeaponData() }
|
||||
});
|
||||
buildCache.FlushAsync().Wait();
|
||||
}
|
||||
|
||||
|
|
@ -175,9 +182,10 @@ public sealed class FireControlTests : IDisposable
|
|||
}
|
||||
|
||||
// A shooter with one in-range, in-arc gun, a detected target at `targetPosition`, and a Minion driving the shooter.
|
||||
private (Ship shooter, Ship target, Zone zone) BuildCombatEncounter(float3 targetPosition, float projectileSpeed = 0f)
|
||||
private (Ship shooter, Ship target, Zone zone) BuildCombatEncounter(float3 targetPosition, float projectileSpeed = 0f, bool twoTargetGuns = false)
|
||||
{
|
||||
var (items, shooter, weapons) = BuildShipWithHardpoints((ItemRotation.None, 0f));
|
||||
var mounts = twoTargetGuns ? new[] { (ItemRotation.None, 0f), (ItemRotation.None, 0f) } : new[] { (ItemRotation.None, 0f) };
|
||||
var (items, shooter, weapons) = BuildShipWithHardpoints(mounts);
|
||||
var weaponData = (InstantWeaponData) weapons[0].Data.Behaviors.Single(b => b is InstantWeaponData);
|
||||
weaponData.Damage = new PerformanceStat { Min = 10, Max = 10 };
|
||||
weaponData.Cooldown = new PerformanceStat { Min = 1, Max = 1 };
|
||||
|
|
@ -194,6 +202,20 @@ public sealed class FireControlTests : IDisposable
|
|||
// gun so the target is a real, ordinarily-constructed ship rather than a workaround-shaped one.
|
||||
var targetGunRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<GearData>("Gun"));
|
||||
Assert.True(target.TryEquip(new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 1 }, new int2(0, 0)));
|
||||
if (twoTargetGuns)
|
||||
{
|
||||
// Both true guns are finite, authored figures: the strong one rates 10, the weak one 1.
|
||||
weaponData.Count = new PerformanceStat { Min = 1, Max = 1 };
|
||||
var weakRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<GearData>("WeakGun"));
|
||||
var weakData = (InstantWeaponData) items.ItemData.GetByName<GearData>("WeakGun").Behaviors.Single();
|
||||
weakData.Damage = new PerformanceStat { Min = 1, Max = 1 };
|
||||
weakData.Count = new PerformanceStat { Min = 1, Max = 1 };
|
||||
weakData.Cooldown = new PerformanceStat { Min = 1, Max = 1 };
|
||||
weakData.Range = new PerformanceStat { Min = 1000, Max = 1000 };
|
||||
weakData.MinRange = new PerformanceStat { Min = 0, Max = 0 };
|
||||
weakData.DamageCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } };
|
||||
Assert.True(target.TryEquip(new EquippableItem { Data = weakRef, Durability = 1, Lot = 2 }, new int2(1, 0)));
|
||||
}
|
||||
zone.Admit(target, piloted: false);
|
||||
|
||||
shooter.Position = float3.zero;
|
||||
|
|
@ -249,7 +271,7 @@ public sealed class FireControlTests : IDisposable
|
|||
zone.Update(1f);
|
||||
Assert.Null(shooter.TargetItem.Value);
|
||||
|
||||
shooter.RecordReveal(target, targetGun, SignatureChannel.LaunchFlash);
|
||||
shooter.RecordReveal(target, targetGun, SignatureChannel.LaunchFlash, targetGun.Data);
|
||||
zone.Update(1f);
|
||||
|
||||
Assert.Same(targetGun, shooter.TargetItem.Value);
|
||||
|
|
@ -262,6 +284,139 @@ public sealed class FireControlTests : IDisposable
|
|||
Assert.Null(shooter.TargetItem.Value);
|
||||
}
|
||||
|
||||
// A design as an observer might be told it is: an instant weapon with rated figures, held in memory (a claim is data,
|
||||
// not a catalog item). Rated damage per second at any range is damage / cooldown (one round, flat curve).
|
||||
private static GearData ClaimedDesign(string name, float damage, float range = 1000f, BezierCurve curve = null) => new GearData
|
||||
{
|
||||
Name = name,
|
||||
Behaviors =
|
||||
{
|
||||
new InstantWeaponData
|
||||
{
|
||||
Damage = new PerformanceStat { Min = 0, Max = damage }, Count = new PerformanceStat { Min = 1, Max = 1 },
|
||||
Cooldown = new PerformanceStat { Min = 1, Max = 1 }, Range = new PerformanceStat { Min = 0, Max = range },
|
||||
MinRange = new PerformanceStat { Min = 0, Max = 0 },
|
||||
DamageCurve = curve ?? new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Ruling ew-aim: the AI classifies from its book, not the target's gear (aetheria-ew, classify-from-belief). A gun
|
||||
// the target claims is not a weapon is not something Combat picks as a weapon, though the gun is truly one and
|
||||
// the shooter has seen it act; clearing the claim and seeing it act again restores the choice. Mutation: Combat
|
||||
// walks target.Equipment (or reveal.Item's true behaviours) instead of RevealedWeapons.
|
||||
[Fact]
|
||||
public void TheAIChoosesFromWhatItBelieves()
|
||||
{
|
||||
var (shooter, target, zone) = BuildCombatEncounter(float3(0, 0, 10));
|
||||
var targetGun = target.Equipment.Single(x => x.Behaviors.Any(b => b is Weapon));
|
||||
|
||||
target.Claim(targetGun, new GearData { Name = "NotAGun" });
|
||||
shooter.RecordReveal(target, targetGun, SignatureChannel.LaunchFlash, target.ClaimOf(targetGun));
|
||||
zone.Update(1f);
|
||||
zone.Update(1f);
|
||||
Assert.Null(shooter.TargetItem.Value);
|
||||
|
||||
target.Claim(targetGun, null);
|
||||
shooter.RecordReveal(target, targetGun, SignatureChannel.LaunchFlash, target.ClaimOf(targetGun));
|
||||
zone.Update(1f);
|
||||
Assert.Same(targetGun, shooter.TargetItem.Value);
|
||||
}
|
||||
|
||||
// Ruling ew-claim-weapon-measure-ruled (rated-design): the AI ranks revealed weapons by their claimed design's
|
||||
// rated figures, so the truly weaker gun claimed as the stronger design is the one chosen, and the unclaimed pair
|
||||
// picks the truly stronger. Mutation: rank by the true weapon's live figure, or by the carrier's stats.
|
||||
[Fact]
|
||||
public void AWeakerGunClaimedAsAStrongerDesignIsChosen()
|
||||
{
|
||||
var (shooter, target, zone) = BuildCombatEncounter(float3(0, 0, 10), twoTargetGuns: true);
|
||||
var strong = target.Equipment.Single(x => x.Data.Name == "Gun");
|
||||
var weak = target.Equipment.Single(x => x.Data.Name == "WeakGun");
|
||||
|
||||
void See()
|
||||
{
|
||||
shooter.RecordReveal(target, strong, SignatureChannel.LaunchFlash, target.ClaimOf(strong));
|
||||
shooter.RecordReveal(target, weak, SignatureChannel.LaunchFlash, target.ClaimOf(weak));
|
||||
zone.Update(1f);
|
||||
zone.Update(1f);
|
||||
}
|
||||
|
||||
See();
|
||||
Assert.Same(strong, shooter.TargetItem.Value); // unclaimed: the truly stronger gun (rated 10 against 1)
|
||||
|
||||
target.Claim(strong, ClaimedDesign("Pea", 2f));
|
||||
target.Claim(weak, ClaimedDesign("Cannon", 90f));
|
||||
See();
|
||||
Assert.Same(weak, shooter.TargetItem.Value);
|
||||
}
|
||||
|
||||
// The ranking reads the claimed design at the distance to the target: a short-range design that out-damages a flat one
|
||||
// up close is worthless at 10 when its Range is 12, and the weaker flat design is chosen. Mutation: Combat halves
|
||||
// targetDistance (the short design then rates 6.2 at 5 and wins), or the rated read ignores the range.
|
||||
[Fact]
|
||||
public void TheAIRanksClaimsAtTheTargetsDistance()
|
||||
{
|
||||
var (shooter, target, zone) = BuildCombatEncounter(float3(0, 0, 10), twoTargetGuns: true);
|
||||
var strong = target.Equipment.Single(x => x.Data.Name == "Gun");
|
||||
var weak = target.Equipment.Single(x => x.Data.Name == "WeakGun");
|
||||
var falling = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 0, 0, 0) } };
|
||||
|
||||
target.Claim(strong, ClaimedDesign("Dart", 10f, range: 12f, curve: falling)); // ~.7 at 10, ~6.2 at 5
|
||||
target.Claim(weak, ClaimedDesign("Slug", 5f)); // 5 at any range
|
||||
shooter.RecordReveal(target, strong, SignatureChannel.LaunchFlash, target.ClaimOf(strong));
|
||||
shooter.RecordReveal(target, weak, SignatureChannel.LaunchFlash, target.ClaimOf(weak));
|
||||
zone.Update(1f);
|
||||
zone.Update(1f);
|
||||
|
||||
Assert.Same(weak, shooter.TargetItem.Value);
|
||||
}
|
||||
|
||||
// Ruling ew-claim-weapon-measure-ruled: no observer reads a target weapon's live state. Two unclaimed guns whose
|
||||
// rated order (10 against 1) is the reverse of their live order (the strong one worn down to a hundredth) are
|
||||
// ranked by the design, so the aim stays on the rated stronger gun. Mutation: rank an unclaimed reveal by the
|
||||
// true weapon's live RangeDamagePerSecond.
|
||||
[Fact]
|
||||
public void TheAIRanksByTheDesignNotTheTargetsLiveGun()
|
||||
{
|
||||
var (shooter, target, zone) = BuildCombatEncounter(float3(0, 0, 10), twoTargetGuns: true);
|
||||
var strong = target.Equipment.Single(x => x.Data.Name == "Gun");
|
||||
var weak = target.Equipment.Single(x => x.Data.Name == "WeakGun");
|
||||
var strongData = (InstantWeaponData) strong.Data.Behaviors.Single(b => b is InstantWeaponData);
|
||||
target.Resolver.AttachModifier(strong, strongData.Damage, "worn", StatModifierType.Multiplier, .01f);
|
||||
|
||||
shooter.RecordReveal(target, strong, SignatureChannel.LaunchFlash, target.ClaimOf(strong));
|
||||
shooter.RecordReveal(target, weak, SignatureChannel.LaunchFlash, target.ClaimOf(weak));
|
||||
zone.Update(1f);
|
||||
zone.Update(1f);
|
||||
|
||||
var liveStrong = strong.Behaviors.OfType<Weapon>().Single().RangeDamagePerSecond(10f);
|
||||
var liveWeak = weak.Behaviors.OfType<Weapon>().Single().RangeDamagePerSecond(10f);
|
||||
Assert.True(liveStrong < liveWeak); // the fixture: live and rated disagree
|
||||
Assert.Same(strong, shooter.TargetItem.Value);
|
||||
}
|
||||
|
||||
// Ties are explicit: two revealed weapons of equal rated figure are taken in the subject's equipment order, whatever
|
||||
// order the shooter saw them in. Mutation: ties go to the last (>=), or the walk follows the book's slot order.
|
||||
[Fact]
|
||||
public void EqualClaimsAreTakenInTheTargetsEquipmentOrder()
|
||||
{
|
||||
var (shooter, target, zone) = BuildCombatEncounter(float3(0, 0, 10), twoTargetGuns: true);
|
||||
var first = target.Equipment.Single(x => x.Data.Name == "Gun");
|
||||
var second = target.Equipment.Single(x => x.Data.Name == "WeakGun");
|
||||
Assert.True(target.Equipment.IndexOf(first) < target.Equipment.IndexOf(second));
|
||||
var design = ClaimedDesign("Twin", 5f);
|
||||
target.Claim(first, design);
|
||||
target.Claim(second, design);
|
||||
|
||||
shooter.RecordReveal(target, second, SignatureChannel.LaunchFlash, target.ClaimOf(second)); // seen first
|
||||
shooter.RecordReveal(target, first, SignatureChannel.LaunchFlash, target.ClaimOf(first));
|
||||
zone.Update(1f);
|
||||
zone.Update(1f);
|
||||
|
||||
Assert.Equal(new[] { first, second }, shooter.RevealedWeapons(target).Select(r => r.Item).ToArray());
|
||||
Assert.Same(first, shooter.TargetItem.Value);
|
||||
}
|
||||
|
||||
// Controls cut (helm split): Combat aims the shooter at the intercept (Aim) and commands the hull toward it
|
||||
// through the one steering law (Turn); it never turns the hull by writing Aim.
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ public sealed partial class FireControlCut124Tests
|
|||
// Fires until a shot lands; returns that outcome and the absorbs the target published for that shot alone.
|
||||
private static (ShotOutcome Outcome, List<AbsorbEvent> Absorbs) FireAtTheMarker(Engagement e, List<IDisposable> subs)
|
||||
{
|
||||
e.Shooter.RecordReveal(e.Target, e.Custom["Marker"], SignatureChannel.LaunchFlash); // the book must hold the item: aiming is earned by seeing it act
|
||||
e.Shooter.RecordReveal(e.Target, e.Custom["Marker"], SignatureChannel.LaunchFlash, e.Custom["Marker"].Data); // the book must hold the item: aiming is earned by seeing it act
|
||||
Assert.True(e.Shooter.TrySelectTargetItem(e.Custom["Marker"]), "fixture: the marker is aimable");
|
||||
var absorbs = Collect(e.Target, subs);
|
||||
ShotOutcome landed = null;
|
||||
|
|
|
|||
|
|
@ -454,7 +454,7 @@ public sealed class SignatureTests : IDisposable
|
|||
var view = r.Ship.VisibilitySources;
|
||||
Assert.False(view is Dictionary<object, Signature>);
|
||||
Assert.True(ReferenceEquals(view, r.Ship.VisibilitySources), "one view object per entity"); // not Assert.Same: its failure message formats the entity graph and aborts the test host
|
||||
Assert.Throws<NotSupportedException>(() => ((IDictionary<object, Signature>) view)[r.Ship] = new Signature(SignatureChannel.Thermal, 1f, null));
|
||||
Assert.Throws<NotSupportedException>(() => ((IDictionary<object, Signature>) view)[r.Ship] = new Signature(SignatureChannel.Thermal, 1f, null, null));
|
||||
}
|
||||
|
||||
// ---- APlumeKeepsTheLargerBurn ----
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ public sealed class TargetingSystemTests : IDisposable
|
|||
Assert.False(observer.TrySelectTargetItem(weapon)); // nothing seen yet
|
||||
Assert.Null(observer.TargetItem.Value);
|
||||
|
||||
observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash);
|
||||
observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash, weapon.Data);
|
||||
Assert.True(observer.TrySelectTargetItem(weapon));
|
||||
Assert.Same(weapon, observer.TargetItem.Value);
|
||||
Assert.Same(weapon, observer.ResolvedTargetItem);
|
||||
|
|
@ -196,7 +196,7 @@ public sealed class TargetingSystemTests : IDisposable
|
|||
var observer = BuildObserver(items, target.Zone);
|
||||
observer.SetTarget(target);
|
||||
|
||||
observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash);
|
||||
observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash, weapon.Data);
|
||||
|
||||
Assert.True(FireControl.IsRevealed(observer, weapon));
|
||||
Assert.False(FireControl.IsRevealed(observer, wideItem));
|
||||
|
|
@ -230,8 +230,8 @@ public sealed class TargetingSystemTests : IDisposable
|
|||
var (items, target, weapon, wideItem, _, _) = BuildTarget(TestSettings());
|
||||
var observer = BuildObserver(items, target.Zone);
|
||||
observer.SetTarget(target);
|
||||
observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash);
|
||||
observer.RecordReveal(target, wideItem, SignatureChannel.LaunchFlash);
|
||||
observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash, weapon.Data);
|
||||
observer.RecordReveal(target, wideItem, SignatureChannel.LaunchFlash, wideItem.Data);
|
||||
Assert.True(observer.TrySelectTargetItem(weapon));
|
||||
|
||||
weapon.EquippableItem.Durability = 0f;
|
||||
|
|
@ -250,7 +250,7 @@ public sealed class TargetingSystemTests : IDisposable
|
|||
var (items, target, weapon, _, _, _) = BuildTarget(TestSettings());
|
||||
var observer = BuildObserver(items, target.Zone);
|
||||
observer.SetTarget(target);
|
||||
observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash);
|
||||
observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash, weapon.Data);
|
||||
Assert.True(FireControl.IsRevealed(observer, weapon));
|
||||
|
||||
target.Deactivate(); // an active ship refuses to unequip
|
||||
|
|
@ -269,7 +269,7 @@ public sealed class TargetingSystemTests : IDisposable
|
|||
var (items, target, weapon, _, _, _) = BuildTarget(TestSettings());
|
||||
var observer = BuildObserver(items, target.Zone);
|
||||
observer.EntityInfoGathered[target] = .5f; // below TargetGearInfoThreshold (.9)
|
||||
observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash);
|
||||
observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash, weapon.Data);
|
||||
Assert.True(FireControl.IsRevealed(observer, weapon));
|
||||
|
||||
weapon.EquippableItem.Durability = 0f;
|
||||
|
|
|
|||
Loading…
Reference in a new issue