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:
parent
ad71eabbaa
commit
a782c7d5d2
6 changed files with 205 additions and 45 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
|
|
@ -15,6 +17,9 @@ public sealed partial class ContactRevealTests
|
|||
|
||||
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 ----
|
||||
|
|
@ -172,46 +177,96 @@ public sealed partial class ContactRevealTests
|
|||
|
||||
// ---- 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, 1000f, 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 and every divisor distinct, so reading Min, the wrong stat or the wrong operator changes the answer.
|
||||
// Mutation: any rated override reads a Min, drops the burst divisor, the charge multiplier or the charge time.
|
||||
// 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 instant = new InstantWeaponData
|
||||
{
|
||||
Damage = Span(10, 100), Count = Span(1, 2), Cooldown = Span(8, 2), MinRange = Span(0, 0), Range = Span(5, 400), DamageCurve = Flat()
|
||||
};
|
||||
Assert.Equal(25f, instant.RatedRangeDamagePerSecond(10f)); // 100 / 2 rounds / 2 s
|
||||
Assert.Equal(400f, instant.RatedRange);
|
||||
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 charged = new ChargedWeaponData
|
||||
{
|
||||
Damage = Span(10, 100), Count = Span(1, 2), Cooldown = Span(8, 2), ChargeTime = Span(1, 3), MinRange = Span(0, 0),
|
||||
Range = Span(5, 400), DamageCurve = Flat(), ChargeFiringDamageMultiplier = 2f
|
||||
};
|
||||
Assert.Equal(20f, charged.RatedRangeDamagePerSecond(10f)); // 100 / 2 * 2 / (2 s + 3 s)
|
||||
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 beam = new ConstantWeaponData { Damage = Span(10, 80), MinRange = Span(0, 0), Range = Span(5, 90), DamageCurve = Flat() };
|
||||
Assert.Equal(80f, beam.RatedRangeDamagePerSecond(10f));
|
||||
Assert.Equal(90f, beam.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.
|
||||
// Mutation: a rated override with its own arithmetic, or a live read that stops calling the shared formula.
|
||||
// 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[] { "Gun", "Beam" });
|
||||
var e = Add(float3(0, 0, 0), new[] { "SpanGun", "SpanCharged", "SpanBeam" });
|
||||
Tick(.1f);
|
||||
var weapons = e.Equipment.Where(i => i.Behaviors.Any(b => b is Weapon)).ToArray();
|
||||
Assert.Equal(2, weapons.Length);
|
||||
foreach (var item in weapons)
|
||||
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();
|
||||
Assert.Equal(100f, live.RangeDamagePerSecond(10f)); // fixture: Damage 100, one round per second
|
||||
Assert.Equal(live.RangeDamagePerSecond(10f), rated.RatedRangeDamagePerSecond(10f));
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ public sealed class FireControlTests : IDisposable
|
|||
|
||||
// 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) => new GearData
|
||||
private static GearData ClaimedDesign(string name, float damage, float range = 1000f, BezierCurve curve = null) => new GearData
|
||||
{
|
||||
Name = name,
|
||||
Behaviors =
|
||||
|
|
@ -294,9 +294,9 @@ public sealed class FireControlTests : IDisposable
|
|||
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 = 1000 },
|
||||
Cooldown = new PerformanceStat { Min = 1, Max = 1 }, Range = new PerformanceStat { Min = 0, Max = range },
|
||||
MinRange = new PerformanceStat { Min = 0, Max = 0 },
|
||||
DamageCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }
|
||||
DamageCurve = curve ?? new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -350,6 +350,73 @@ public sealed class FireControlTests : IDisposable
|
|||
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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue