Merge eureka/aetheria-release-quality-readers (head e95a3657)
Every reader of item quality reads Lot.EffectiveQuality (ruling effective-quality-mean-of-roles). Reports cut-quality-readers.h2, h3; Soul s1, s2 hold. Master's commits since base 70c7a497 are docs only, so the merged code equals the verified head.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
67517f407f
8 changed files with 261 additions and 37 deletions
|
|
@ -2189,16 +2189,17 @@ public class EquippedItem : IStatContext
|
|||
Hardpoint = hardpoint;
|
||||
EquippableItem = item;
|
||||
Lot = ItemManager.GetLot(item);
|
||||
var quality = Lot.EffectiveQuality();
|
||||
Position = position;
|
||||
Conductivity = Data.Conductivity;
|
||||
ThermalExponent = lerp(
|
||||
ItemManager.GameplaySettings.ThermalQualityMin,
|
||||
ItemManager.GameplaySettings.ThermalQualityMax,
|
||||
pow(Lot.Quality, ItemManager.GameplaySettings.ThermalQualityExponent));
|
||||
pow(quality, ItemManager.GameplaySettings.ThermalQualityExponent));
|
||||
DurabilityExponent = lerp(
|
||||
ItemManager.GameplaySettings.DurabilityQualityMin,
|
||||
ItemManager.GameplaySettings.DurabilityQualityMax,
|
||||
pow(Lot.Quality, ItemManager.GameplaySettings.DurabilityQualityExponent));
|
||||
pow(quality, ItemManager.GameplaySettings.DurabilityQualityExponent));
|
||||
var hullData = itemManager.GetData(entity.Hull);
|
||||
InsetShape = hullData.Shape.Inset(Data.Shape, position, item.Rotation);
|
||||
if (Entity.Temperature != null) oldTemperature = Temperature;
|
||||
|
|
@ -2369,7 +2370,7 @@ public class EquippedItem : IStatContext
|
|||
Entity.Resolver.InvalidateSource(this, StatSource.Durability);
|
||||
var performanceThreshold = Entity.Settings.ShutdownPerformance;
|
||||
Wear = (1 - pow(ThermalPerformance,
|
||||
(1 - pow(Lot.Quality, ItemManager.GameplaySettings.QualityWearExponent)) *
|
||||
(1 - pow(Lot.EffectiveQuality(), ItemManager.GameplaySettings.QualityWearExponent)) *
|
||||
ItemManager.GameplaySettings.ThermalWearExponent) +
|
||||
deltaTemp * ItemManager.GameplaySettings.DeltaTempWearExponent
|
||||
) * Data.Durability / Data.ThermalResilience;
|
||||
|
|
|
|||
|
|
@ -688,7 +688,7 @@ public class StatTerm
|
|||
{
|
||||
[JsonProperty("source"), Key(0)] public StatSource Source;
|
||||
[JsonProperty("exponent"), Key(1)] public float Exponent;
|
||||
// The design role whose part quality this term reads; unset reads the item's own workmanship quality.
|
||||
// The design role whose part quality this term reads; unset reads the item's effective quality (Lot.EffectiveQuality).
|
||||
// Njordr states the same rule as a derivation over dimensions; this is that rule with one dimension.
|
||||
[Inspectable, JsonProperty("role"), Key(2)] public string Role;
|
||||
}
|
||||
|
|
@ -888,7 +888,7 @@ public static class StatValidation
|
|||
}
|
||||
|
||||
// Cut 7 (docs/stats-and-power-cut.md): "a stat naming a role its design lacks" must be refused, not silently
|
||||
// fall back to the lot's own workmanship (Lot.QualityForRole already falls back for a role a lot never rolled
|
||||
// fall back to the lot's effective quality (Lot.QualityForRole already falls back for a role a lot never rolled
|
||||
// -- that is the intended forward-compat path for an old lot meeting a newly-authored role. This is the other
|
||||
// direction: a StatTerm naming a role the design itself never declared is an authoring mistake, not a lot
|
||||
// predating the role, and must fail loudly rather than silently reading generic quality). Every PerformanceStat
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ public class ItemManager
|
|||
return result;
|
||||
}
|
||||
|
||||
// The unequipped case: quality and durability read live off the item and its lot; heat and modifiers do not
|
||||
// The unequipped case: quality (the lot's effective quality) and durability read live off the item and its lot; heat and modifiers do not
|
||||
// exist without an entity, so they resolve as the identity for their term rather than a bespoke formula.
|
||||
private sealed class UnequippedStatContext : IStatContext
|
||||
{
|
||||
|
|
@ -99,7 +99,7 @@ public class ItemManager
|
|||
_durabilityExponent = lerp(
|
||||
items.GameplaySettings.DurabilityQualityMin,
|
||||
items.GameplaySettings.DurabilityQualityMax,
|
||||
pow(Lot.Quality, items.GameplaySettings.DurabilityQualityExponent));
|
||||
pow(Lot.EffectiveQuality(), items.GameplaySettings.DurabilityQualityExponent));
|
||||
}
|
||||
|
||||
public float HeatFactor(float exponent) => 1f;
|
||||
|
|
@ -111,10 +111,11 @@ public class ItemManager
|
|||
public float ConstantModifier(PerformanceStat stat) => 0f;
|
||||
}
|
||||
|
||||
// Price follows effective quality (price-follows-effective-quality), so a sleeper's over-specced parts do not show in it.
|
||||
public int GetPrice(CraftedItemInstance item)
|
||||
{
|
||||
var data = GetData(item);
|
||||
return (int) (GameplaySettings.QualityPriceModifier.Evaluate(GetLot(item).Quality) * data.Price);
|
||||
return (int) (GameplaySettings.QualityPriceModifier.Evaluate(GetLot(item).EffectiveQuality()) * data.Price);
|
||||
}
|
||||
|
||||
// Builds one of a manufacturer's branded products (quality-is-assembly-over-supply-chain): the unit's own quality is its
|
||||
|
|
@ -222,9 +223,10 @@ public class ItemManager
|
|||
public string Title(ItemInstance item) =>
|
||||
(item is CraftedItemInstance crafted ? Brand(crafted).Product?.Name : null) ?? GetData(item).Name;
|
||||
|
||||
// The tier name and colour follow price, so they hide the same sleepers.
|
||||
public (RarityTier tier, int upgrades) GetTier(CraftedItemInstance item)
|
||||
{
|
||||
var quality = GetLot(item).Quality;
|
||||
var quality = GetLot(item).EffectiveQuality();
|
||||
var tier = GameplaySettings.Tiers[0];
|
||||
foreach (var t in GameplaySettings.Tiers)
|
||||
if (quality + .001f > t.Quality)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ public class ProvenanceLedger
|
|||
}
|
||||
}
|
||||
|
||||
// One lot: the design it built, who made it and from what, its rolled workmanship, how well each design role
|
||||
// One lot: the design it built, who made it and from what, its final assembly quality, how well each design role
|
||||
// was filled, and the branded product it was made as. Immutable once minted; instances carry a LotId, never a copy.
|
||||
[MessagePackObject]
|
||||
public class Lot
|
||||
|
|
@ -74,18 +74,28 @@ public class Lot
|
|||
|
||||
// The quality a stat reads for one of the design's roles: the part's, capped by the lot's own final assembly
|
||||
// (quality-lean-assembly-hard-ceiling), so a part better than its assembly is worth the assembly. An unnamed role, a
|
||||
// design without roles, and a lot minted before roles existed all fall back to the lot's own workmanship.
|
||||
// design without roles, and a role without a fill read the item's effective quality.
|
||||
public float QualityForRole(string role)
|
||||
{
|
||||
if (string.IsNullOrEmpty(role) || Roles == null) return Quality;
|
||||
if (string.IsNullOrEmpty(role) || Roles == null) return EffectiveQuality();
|
||||
foreach (var fill in Roles)
|
||||
if (fill.Role == role) return Math.Min(fill.Quality, Quality);
|
||||
return Quality;
|
||||
return EffectiveQuality();
|
||||
}
|
||||
|
||||
// The item's one quality (effective-quality-mean-of-roles): the mean over its role fills of the part capped by the final
|
||||
// assembly. Price, tier, heat, durability and wear all read it; a roleless lot reads its assembly.
|
||||
public float EffectiveQuality()
|
||||
{
|
||||
if (Roles == null || Roles.Count == 0) return Quality;
|
||||
var sum = 0f;
|
||||
foreach (var fill in Roles) sum += Math.Min(fill.Quality, Quality);
|
||||
return sum / Roles.Count;
|
||||
}
|
||||
}
|
||||
|
||||
// One filled slot: the design's role name and how good the part that went into it turned out to be. Stats read it through
|
||||
// Lot.QualityForRole, capped by the lot's assembly; the raw value is kept for disassembly (named-components-after-demo).
|
||||
// Lot.QualityForRole and Lot.EffectiveQuality, capped by the lot's assembly; the raw value is kept for disassembly (named-components-after-demo).
|
||||
[MessagePackObject]
|
||||
public class RoleFill
|
||||
{
|
||||
|
|
|
|||
|
|
@ -184,15 +184,41 @@ public sealed class AssemblyFromMakerTests : IDisposable
|
|||
Assert.All(Mint(tight, 50), q => Assert.Equal(.5f, q, 3));
|
||||
}
|
||||
|
||||
// The fallbacks still read the assembly: an unnamed role, an unrolled role, and a lot with no roles.
|
||||
// The fallbacks read the item's effective quality: an unnamed role and an unfilled role on a lot whose fills sit below and
|
||||
// above its assembly (lens .3 -> .3, frame .9 -> .6, mean .45), and a lot with no roles reading its assembly. A found
|
||||
// fill still reads its own capped part.
|
||||
[Fact]
|
||||
public void FallbacksReadTheAssembly()
|
||||
public void FallbacksReadEffectiveQuality()
|
||||
{
|
||||
var lot = new Lot { Quality = .4f, Roles = new List<RoleFill> { new RoleFill { Role = "lens", Quality = .9f } } };
|
||||
Assert.Equal(.4f, lot.QualityForRole(null), 3);
|
||||
Assert.Equal(.4f, lot.QualityForRole(""), 3);
|
||||
Assert.Equal(.4f, lot.QualityForRole("ghost"), 3);
|
||||
var lot = new Lot
|
||||
{
|
||||
Quality = .6f,
|
||||
Roles = new List<RoleFill> { new RoleFill { Role = "lens", Quality = .3f }, new RoleFill { Role = "frame", Quality = .9f } }
|
||||
};
|
||||
Assert.Equal(.45f, lot.QualityForRole(null), 3);
|
||||
Assert.Equal(.45f, lot.QualityForRole(""), 3);
|
||||
Assert.Equal(.45f, lot.QualityForRole("ghost"), 3);
|
||||
Assert.Equal(.3f, lot.QualityForRole("lens"), 3);
|
||||
Assert.Equal(.4f, new Lot { Quality = .4f }.QualityForRole("lens"), 3);
|
||||
Assert.Equal(.4f, new Lot { Quality = .4f, Roles = new List<RoleFill>() }.QualityForRole("lens"), 3);
|
||||
}
|
||||
|
||||
// The effective quality is the mean over the fills of min(part, assembly): assembly .6 with parts .9, .3, .5 reads
|
||||
// (.6 + .3 + .5) / 3, not the raw mean .5667, the min .3, the assembly .6 or the max. A lot with no fills reads its assembly.
|
||||
[Fact]
|
||||
public void EffectiveQualityIsTheMeanOfCappedFills()
|
||||
{
|
||||
var lot = new Lot
|
||||
{
|
||||
Quality = .6f,
|
||||
Roles = new List<RoleFill>
|
||||
{
|
||||
new RoleFill { Role = "a", Quality = .9f }, new RoleFill { Role = "b", Quality = .3f }, new RoleFill { Role = "c", Quality = .5f }
|
||||
}
|
||||
};
|
||||
Assert.Equal(.4667f, lot.EffectiveQuality(), 3);
|
||||
Assert.Equal(.6f, new Lot { Quality = .6f }.EffectiveQuality(), 3);
|
||||
Assert.Equal(.6f, new Lot { Quality = .6f, Roles = new List<RoleFill>() }.EffectiveQuality(), 3);
|
||||
}
|
||||
|
||||
// A Quality term on a role with part .9 and assembly .4 evaluates as if quality were .4, on a real minted instance.
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ public sealed class HeatResponseTests : IDisposable
|
|||
|
||||
// A hull plus one thermal gear design, equipped and settled at the given start temperature (two
|
||||
// UpdatePerformance calls so deltaTemp reads 0 before the test drives its own change).
|
||||
private EquippedItem BuildEquippedAt(CultCache cache, ItemManager items, float startTemperature, out Ship ship)
|
||||
private EquippedItem BuildEquippedAt(CultCache cache, ItemManager items, float startTemperature, out Ship ship, Lot gearLot = null)
|
||||
{
|
||||
var maker = cache.RefOf(cache.GetByName<Faction>("Maker"));
|
||||
var hullData = cache.GetByName<HullData>("Skiff");
|
||||
var thermalData = cache.GetByName<GearData>("Thermal");
|
||||
var hullItem = (EquippableItem) items.CreateInstance(items.CreateLot(hullData, maker, .5f));
|
||||
ship = new Ship(items, null, hullItem, new EntitySettings());
|
||||
var gearItem = (EquippableItem) items.CreateInstance(items.CreateLot(thermalData, maker, .5f));
|
||||
var gearItem = (EquippableItem) items.CreateInstance(gearLot != null ? items.Lots.Add(gearLot) : items.CreateLot(thermalData, maker, .5f));
|
||||
Assert.True(ship.TryEquip(gearItem, HardpointCell));
|
||||
var equipped = ship.Equipment.Single(e => e.Data is GearData);
|
||||
foreach (var cell in equipped.InsetShape.Coordinates) ship.Temperature[cell.x, cell.y] = startTemperature;
|
||||
|
|
@ -249,6 +249,27 @@ public sealed class HeatResponseTests : IDisposable
|
|||
Assert.True(equipped.Wear > 0f);
|
||||
}
|
||||
|
||||
// Wear's quality term reads the lot's effective quality: a .9 lot whose one part is .1 wears as a roleless .1 lot, and not
|
||||
// as a roleless .9 lot, which wears less (better quality wears less).
|
||||
[Fact]
|
||||
public void HeldOutsidePlateauWearReadsEffectiveQuality()
|
||||
{
|
||||
using var cache = OpenCatalogWithThermalGear();
|
||||
var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { });
|
||||
var thermal = cache.RefOf<ItemData>(cache.GetByName<GearData>("Thermal"));
|
||||
Lot Gear(float assembly, params float[] parts) => new Lot
|
||||
{
|
||||
Design = thermal, Origin = new Attributed(), Quality = assembly,
|
||||
Roles = parts.Select(p => new RoleFill { Role = "core", Quality = p }).ToList()
|
||||
};
|
||||
float WearAt10(Lot lot) => BuildEquippedAt(cache, items, 10f, out _, lot).Wear;
|
||||
|
||||
var sleeper = WearAt10(Gear(.9f, .1f));
|
||||
Assert.Equal(WearAt10(Gear(.1f)), sleeper, 5);
|
||||
Assert.NotEqual(WearAt10(Gear(.9f)), sleeper, 5);
|
||||
Assert.True(WearAt10(Gear(.9f)) < sleeper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AFastSwingAcrossThePlateauStillWears()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -490,7 +490,8 @@ public sealed class LoadoutTests : IDisposable
|
|||
}
|
||||
|
||||
// A Lamp lot with Quality .2 and one role fill (lens .15, under the assembly): a stat reading that role gets .15, one
|
||||
// reading none gets .2, and GetTier reads .2.
|
||||
// reading none gets the effective quality .15, and GetTier reads .2. A lot of
|
||||
// assembly .6 with a lens of .3 is Common, not Rare: the tier follows its effective quality.
|
||||
[Fact]
|
||||
public void StatsReadTheLot()
|
||||
{
|
||||
|
|
@ -519,15 +520,30 @@ public sealed class LoadoutTests : IDisposable
|
|||
Quality = .6f,
|
||||
Roles = new List<RoleFill>()
|
||||
});
|
||||
var cappedLot = items.Lots.Add(new Lot
|
||||
{
|
||||
Design = cache.RefOf<ItemData>(lamp),
|
||||
Origin = new Attributed(),
|
||||
Quality = .6f,
|
||||
Roles = new List<RoleFill> { new RoleFill { Role = "lens", Quality = .3f } }
|
||||
});
|
||||
// Just under the Rare threshold, inside GetTier's .001 tolerance.
|
||||
var nearRareLot = items.Lots.Add(new Lot { Design = cache.RefOf<ItemData>(lamp), Origin = new Attributed(), Quality = .4995f, Roles = new List<RoleFill>() });
|
||||
var instance = (EquippableItem) items.CreateInstance(commonLot);
|
||||
var rareInstance = (EquippableItem) items.CreateInstance(rareLot);
|
||||
var cappedInstance = (EquippableItem) items.CreateInstance(cappedLot);
|
||||
var nearRareInstance = (EquippableItem) items.CreateInstance(nearRareLot);
|
||||
|
||||
var withRole = new PerformanceStat { Min = 0, Max = 1, Terms = { new StatTerm { Source = StatSource.Quality, Exponent = 1, Role = "lens" } } };
|
||||
var noRole = new PerformanceStat { Min = 0, Max = 1, Terms = { new StatTerm { Source = StatSource.Quality, Exponent = 1 } } };
|
||||
Assert.Equal(.15f, items.Evaluate(withRole, instance), 3);
|
||||
Assert.Equal(.2f, items.Evaluate(noRole, instance), 3);
|
||||
Assert.Equal(.15f, items.Evaluate(noRole, instance), 3);
|
||||
Assert.Equal(.2f, items.GetTier(instance).tier.Quality, 3);
|
||||
Assert.Equal(.5f, items.GetTier(rareInstance).tier.Quality, 3);
|
||||
Assert.Equal(.2f, items.GetTier(cappedInstance).tier.Quality, 3);
|
||||
Assert.Equal(.5f, items.GetTier(nearRareInstance).tier.Quality, 3);
|
||||
// Upgrades count the steps of .05 above the tier: the capped lot's effective .3 is two above Common's .2.
|
||||
Assert.Equal(2, items.GetTier(cappedInstance).upgrades);
|
||||
|
||||
// Minting a second instance from an existing lot must not re-roll the lot's quality.
|
||||
var secondInstance = (EquippableItem) items.CreateInstance(commonLot);
|
||||
|
|
@ -535,6 +551,79 @@ public sealed class LoadoutTests : IDisposable
|
|||
Assert.Equal(.2f, items.GetTier(secondInstance).tier.Quality, 3);
|
||||
}
|
||||
|
||||
// The shipped tiers (Settings.asset) band a lot's effective quality, so the tier hides a sleeper whose part sits above
|
||||
// its assembly (price-follows-effective-quality). Assembly at the fourth threshold, parts at the top and bottom
|
||||
// thresholds: the capped mean falls in the second band, where the uncapped part mean reaches the third, the assembly
|
||||
// alone the fourth and the best part the fifth.
|
||||
[Fact]
|
||||
public void GetTierHidesAnOverSpecPart()
|
||||
{
|
||||
using var cache = Open();
|
||||
var settings = AuthoredSettings.Load(RunStartTests.FindRepoRoot()).Read<GameplaySettings>("GameplaySettings");
|
||||
var tiers = settings.Tiers;
|
||||
var items = new ItemManager(cache, new ProvenanceLedger(), settings, _ => { });
|
||||
var lamp = cache.GetByName<GearData>("Lamp");
|
||||
var assembly = tiers[3].Quality;
|
||||
var sleeperLot = items.Lots.Add(new Lot
|
||||
{
|
||||
Design = cache.RefOf<ItemData>(lamp),
|
||||
Origin = new Attributed(),
|
||||
Quality = assembly,
|
||||
Roles = new List<RoleFill>
|
||||
{
|
||||
new RoleFill { Role = "lens", Quality = tiers[4].Quality },
|
||||
new RoleFill { Role = "frame", Quality = tiers[0].Quality }
|
||||
}
|
||||
});
|
||||
var cappedMean = (assembly + tiers[0].Quality) / 2f;
|
||||
var rolelessLot = items.Lots.Add(new Lot { Design = cache.RefOf<ItemData>(lamp), Origin = new Attributed(), Quality = cappedMean, Roles = new List<RoleFill>() });
|
||||
|
||||
var sleeper = items.GetTier((CraftedItemInstance) items.CreateInstance(sleeperLot));
|
||||
var roleless = items.GetTier((CraftedItemInstance) items.CreateInstance(rolelessLot));
|
||||
Assert.Equal(tiers[1].Name, sleeper.tier.Name);
|
||||
Assert.Equal(roleless.tier.Name, sleeper.tier.Name);
|
||||
Assert.Equal(roleless.upgrades, sleeper.upgrades);
|
||||
}
|
||||
|
||||
// The tolerance is open: a quality a full .001 under a threshold stays in the tier below, one inside it reaches the
|
||||
// threshold's tier. Upgrades count steps of .0499 above the tier's threshold. Every value comes from the shipped tiers.
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
[InlineData(4)]
|
||||
public void GetTierToleranceIsOpenAtEveryShippedThreshold(int index)
|
||||
{
|
||||
using var cache = Open();
|
||||
var settings = AuthoredSettings.Load(RunStartTests.FindRepoRoot()).Read<GameplaySettings>("GameplaySettings");
|
||||
var tiers = settings.Tiers;
|
||||
Assert.True(tiers.Length > index, "premise: the shipped asset carries this tier");
|
||||
var items = new ItemManager(cache, new ProvenanceLedger(), settings, _ => { });
|
||||
var lamp = cache.GetByName<GearData>("Lamp");
|
||||
string TierAt(float quality) => items.GetTier((CraftedItemInstance) items.CreateInstance(items.Lots.Add(
|
||||
new Lot { Design = cache.RefOf<ItemData>(lamp), Origin = new Attributed(), Quality = quality, Roles = new List<RoleFill>() }))).tier.Name;
|
||||
|
||||
var threshold = tiers[index].Quality;
|
||||
Assert.Equal(tiers[index - 1].Name, TierAt(threshold - .001f));
|
||||
Assert.Equal(tiers[index].Name, TierAt(threshold - .0005f));
|
||||
Assert.Equal(tiers[index].Name, TierAt(threshold));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTierUpgradesAreStepsOfFortyNineNinetyNinePerTenThousand()
|
||||
{
|
||||
using var cache = Open();
|
||||
var settings = AuthoredSettings.Load(RunStartTests.FindRepoRoot()).Read<GameplaySettings>("GameplaySettings");
|
||||
var items = new ItemManager(cache, new ProvenanceLedger(), settings, _ => { });
|
||||
var lamp = cache.GetByName<GearData>("Lamp");
|
||||
// Two steps and a hair above the lowest threshold: the step width decides whether that counts as two or one.
|
||||
var quality = settings.Tiers[0].Quality + 2 * .0499f + .0001f;
|
||||
var lot = items.Lots.Add(new Lot { Design = cache.RefOf<ItemData>(lamp), Origin = new Attributed(), Quality = quality, Roles = new List<RoleFill>() });
|
||||
var result = items.GetTier((CraftedItemInstance) items.CreateInstance(lot));
|
||||
Assert.Equal(settings.Tiers[0].Name, result.tier.Name);
|
||||
Assert.Equal(2, result.upgrades);
|
||||
}
|
||||
|
||||
// S7 (docs/stats-and-power-cut.md Cut 1 findings): a Quality term must raise the lot's quality to its own
|
||||
// declared Exponent, not always to 1. StatsReadTheLot uses Exponent = 1 throughout, which cannot distinguish
|
||||
// "read the term's exponent" from "always use 1" -- pow(x, 1) == x either way. Exponent = 2 here can: a
|
||||
|
|
@ -553,9 +642,11 @@ public sealed class LoadoutTests : IDisposable
|
|||
Assert.Equal(.25f, items.Evaluate(squared, instance), 3);
|
||||
}
|
||||
|
||||
// F4: GetPrice reads the lot's quality through GameplaySettings.QualityPriceModifier.
|
||||
// F4: GetPrice reads the lot's effective quality through GameplaySettings.QualityPriceModifier: a lot of assembly .8 whose
|
||||
// one part is .2 prices exactly as a roleless .2 lot (price-follows-effective-quality), and a lot of assembly .6 with
|
||||
// parts .9 and .2 (capped to .6 and .2) prices as a roleless .4 lot: the mean of the capped parts, not the best part.
|
||||
[Fact]
|
||||
public void GetPriceReadsLotQuality()
|
||||
public void GetPriceReadsEffectiveQuality()
|
||||
{
|
||||
using var cache = Open();
|
||||
var settings = RunSaveTests.TestSettings();
|
||||
|
|
@ -564,16 +655,34 @@ public sealed class LoadoutTests : IDisposable
|
|||
var lamp = cache.GetByName<GearData>("Lamp");
|
||||
var lowLot = items.Lots.Add(new Lot { Design = cache.RefOf<ItemData>(lamp), Origin = new Attributed(), Quality = .2f, Roles = new List<RoleFill>() });
|
||||
var highLot = items.Lots.Add(new Lot { Design = cache.RefOf<ItemData>(lamp), Origin = new Attributed(), Quality = .8f, Roles = new List<RoleFill>() });
|
||||
var sleeperLot = items.Lots.Add(new Lot
|
||||
{
|
||||
Design = cache.RefOf<ItemData>(lamp), Origin = new Attributed(), Quality = .8f,
|
||||
Roles = new List<RoleFill> { new RoleFill { Role = "lens", Quality = .2f } }
|
||||
});
|
||||
var middleLot = items.Lots.Add(new Lot { Design = cache.RefOf<ItemData>(lamp), Origin = new Attributed(), Quality = .4f, Roles = new List<RoleFill>() });
|
||||
var mixedLot = items.Lots.Add(new Lot
|
||||
{
|
||||
Design = cache.RefOf<ItemData>(lamp), Origin = new Attributed(), Quality = .6f,
|
||||
Roles = new List<RoleFill> { new RoleFill { Role = "lens", Quality = .9f }, new RoleFill { Role = "frame", Quality = .2f } }
|
||||
});
|
||||
var low = (EquippableItem) items.CreateInstance(lowLot);
|
||||
var high = (EquippableItem) items.CreateInstance(highLot);
|
||||
var sleeper = (EquippableItem) items.CreateInstance(sleeperLot);
|
||||
var middle = (EquippableItem) items.CreateInstance(middleLot);
|
||||
var mixed = (EquippableItem) items.CreateInstance(mixedLot);
|
||||
Assert.True(items.GetPrice(high) > items.GetPrice(low));
|
||||
Assert.Equal(items.GetPrice(low), items.GetPrice(sleeper));
|
||||
Assert.Equal(items.GetPrice(middle), items.GetPrice(mixed));
|
||||
Assert.True(items.GetPrice(middle) > items.GetPrice(low));
|
||||
Assert.True(items.GetPrice(sleeper) < items.GetPrice(high));
|
||||
}
|
||||
|
||||
// F4: the durability exponent path in ItemManager.Evaluate reads the lot's quality (GameplaySettings.
|
||||
// F4: the durability exponent path in ItemManager.Evaluate reads the lot's effective quality (GameplaySettings.
|
||||
// DurabilityQuality{Min,Max,Exponent}), independently of the quality-for-role term (absent here -- the
|
||||
// stat declares only a Durability term).
|
||||
// stat declares only a Durability term): a .9 lot whose one part is .1 evaluates as a roleless .1 lot.
|
||||
[Fact]
|
||||
public void EvaluateDurabilityExponentReadsLotQuality()
|
||||
public void EvaluateDurabilityExponentReadsEffectiveQuality()
|
||||
{
|
||||
using var cache = AetheriaStores.Open(Catalog, catalogWritable: true);
|
||||
var durable = cache.Upsert(new GearData { Name = "Durable", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 10, Durability = 100 });
|
||||
|
|
@ -582,19 +691,29 @@ public sealed class LoadoutTests : IDisposable
|
|||
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
||||
var lowLot = items.Lots.Add(new Lot { Design = new CultRecordRef<ItemData>(durable.Key), Origin = new Attributed(), Quality = .1f, Roles = new List<RoleFill>() });
|
||||
var highLot = items.Lots.Add(new Lot { Design = new CultRecordRef<ItemData>(durable.Key), Origin = new Attributed(), Quality = .9f, Roles = new List<RoleFill>() });
|
||||
var sleeperLot = items.Lots.Add(new Lot
|
||||
{
|
||||
Design = new CultRecordRef<ItemData>(durable.Key), Origin = new Attributed(), Quality = .9f,
|
||||
Roles = new List<RoleFill> { new RoleFill { Role = "lens", Quality = .1f } }
|
||||
});
|
||||
var low = (EquippableItem) items.CreateInstance(lowLot);
|
||||
var high = (EquippableItem) items.CreateInstance(highLot);
|
||||
var sleeper = (EquippableItem) items.CreateInstance(sleeperLot);
|
||||
low.Durability = items.GetData(low).Durability / 2;
|
||||
high.Durability = items.GetData(high).Durability / 2;
|
||||
sleeper.Durability = items.GetData(sleeper).Durability / 2;
|
||||
|
||||
var stat = new PerformanceStat { Min = 0, Max = 1, Terms = { new StatTerm { Source = StatSource.Durability, Exponent = 1 } } };
|
||||
Assert.NotEqual(items.Evaluate(stat, low), items.Evaluate(stat, high));
|
||||
Assert.Equal(items.Evaluate(stat, low), items.Evaluate(stat, sleeper), 3);
|
||||
Assert.NotEqual(items.Evaluate(stat, high), items.Evaluate(stat, sleeper), 3);
|
||||
}
|
||||
|
||||
// F4: EquippedItem's thermal exponent (Entity.cs's EquippedItem constructor) reads the lot's quality
|
||||
// (GameplaySettings.ThermalQuality{Min,Max,Exponent}), through a real equip on a live entity.
|
||||
// F4: EquippedItem's thermal and durability exponents (Entity.cs's EquippedItem constructor) read the lot's effective
|
||||
// quality (GameplaySettings.{Thermal,Durability}Quality{Min,Max,Exponent}), through a real equip on a live entity: a .9
|
||||
// lot whose one part is .1 equips as a roleless .1 lot.
|
||||
[Fact]
|
||||
public void EquippedItemThermalExponentReadsLotQuality()
|
||||
public void EquippedItemExponentsReadEffectiveQuality()
|
||||
{
|
||||
using var cache = Open();
|
||||
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
||||
|
|
@ -604,18 +723,31 @@ public sealed class LoadoutTests : IDisposable
|
|||
|
||||
// TestSettings has one tier, so materializing through a Loadout always rolls the same quality; mint the
|
||||
// gear's lot directly at a chosen quality and equip it onto a bare ship instead.
|
||||
Ship BuildWithGearQuality(float quality)
|
||||
Ship BuildWithGear(Func<int> mintGear)
|
||||
{
|
||||
var hullItem = (EquippableItem) items.CreateInstance(items.CreateLot(hullData, maker, .5f));
|
||||
var ship = new Ship(items, null, hullItem, new EntitySettings());
|
||||
var gearItem = (EquippableItem) items.CreateInstance(items.CreateLot(lampData, maker, quality));
|
||||
var gearItem = (EquippableItem) items.CreateInstance(mintGear());
|
||||
Assert.True(ship.TryEquip(gearItem, HardpointCell));
|
||||
return ship;
|
||||
}
|
||||
Ship BuildWithGearQuality(float quality) => BuildWithGear(() => items.CreateLot(lampData, maker, quality));
|
||||
// The gear is a lot added directly: assembly .9, one part at .1.
|
||||
Ship BuildWithSleeperGear() => BuildWithGear(() => items.Lots.Add(new Lot
|
||||
{
|
||||
Design = cache.RefOf<ItemData>(lampData), Origin = new Attributed(), Quality = .9f,
|
||||
Roles = new List<RoleFill> { new RoleFill { Role = "lens", Quality = .1f } }
|
||||
}));
|
||||
|
||||
var lowEquipped = BuildWithGearQuality(.1f).Equipment.Single(e => e.Data is GearData);
|
||||
var highEquipped = BuildWithGearQuality(.9f).Equipment.Single(e => e.Data is GearData);
|
||||
var sleeperEquipped = BuildWithSleeperGear().Equipment.Single(e => e.Data is GearData);
|
||||
Assert.NotEqual(lowEquipped.ThermalExponent, highEquipped.ThermalExponent);
|
||||
Assert.NotEqual(lowEquipped.DurabilityExponent, highEquipped.DurabilityExponent);
|
||||
Assert.Equal(lowEquipped.ThermalExponent, sleeperEquipped.ThermalExponent, 5);
|
||||
Assert.Equal(lowEquipped.DurabilityExponent, sleeperEquipped.DurabilityExponent, 5);
|
||||
Assert.NotEqual(highEquipped.ThermalExponent, sleeperEquipped.ThermalExponent, 5);
|
||||
Assert.NotEqual(highEquipped.DurabilityExponent, sleeperEquipped.DurabilityExponent, 5);
|
||||
}
|
||||
|
||||
// Cut 0 (docs/stats-and-power-cut.md): the collapsed evaluation path (PerformanceStat.Evaluate(IStatContext))
|
||||
|
|
|
|||
|
|
@ -38,17 +38,49 @@ public sealed partial class RunStartTests
|
|||
return degrees(SignedTurn(before, ship.Direction));
|
||||
}
|
||||
|
||||
// The two Large Drives, refitted with lots staged at the low and high end of the makers' assembly range (.55 and .85, each
|
||||
// with its parts at its assembly), so their effective qualities differ by construction. The lots are fitted through the
|
||||
// ship's own unequip and equip (an active entity refuses both, so it is deactivated for the refit and reactivated), so
|
||||
// heat, thrust and the allocator stay the production path.
|
||||
private void StageDriveLots(Ship ship)
|
||||
{
|
||||
ship.Deactivate();
|
||||
var drives = AllocDrives(ship).OrderBy(d => d.Item.Position.x).ToArray();
|
||||
Assert.Equal(2, drives.Length);
|
||||
var assemblies = new[] { .55f, .85f };
|
||||
for (var i = 0; i < drives.Length; i++)
|
||||
{
|
||||
var fitted = drives[i].Item;
|
||||
var unit = fitted.EquippableItem;
|
||||
var lot = _items.GetLot(unit);
|
||||
Assert.NotNull(ship.TryUnequip(fitted));
|
||||
var staged = new EquippableItem
|
||||
{
|
||||
Data = unit.Data, Durability = unit.Durability, Rotation = unit.Rotation,
|
||||
Lot = _items.Lots.Add(new Lot
|
||||
{
|
||||
Design = lot.Design, Origin = lot.Origin, Product = lot.Product, Quality = assemblies[i],
|
||||
Roles = (lot.Roles ?? new List<RoleFill>()).Select(r => new RoleFill { Role = r.Role, Quality = assemblies[i] }).ToList()
|
||||
})
|
||||
};
|
||||
Assert.True(ship.TryEquip(staged, fitted.Position));
|
||||
}
|
||||
ship.Activate();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MismatchedDrivesHoldHeading()
|
||||
{
|
||||
var (_, player) = AllocDuel();
|
||||
StageDriveLots(player);
|
||||
var drives = AllocDrives(player);
|
||||
player.MovementDirection = float2(0, 1);
|
||||
player.Turn = 0;
|
||||
Assert.True(abs(AllocFly(player, 120)) < .1f);
|
||||
var heading = AllocFly(player, 120);
|
||||
// The drives start alike and heat apart as they fire: by now they are mismatched and both pushing.
|
||||
Assert.True(drives.Length >= 2 && drives[^1].NominalThrust > drives[0].NominalThrust * 1.05f, "fixture: the drives are mismatched");
|
||||
Assert.All(drives, d => Assert.True(d.Axis > .5f, "fixture: the drives are pushing"));
|
||||
Assert.True(drives.Length >= 2 && drives[^1].NominalThrust > drives[0].NominalThrust * 1.05f, "premise: the drives' NominalThrust spread reaches 5 percent");
|
||||
Assert.All(drives, d => Assert.True(d.Axis > .5f, "premise: the drives are pushing"));
|
||||
Assert.True(abs(heading) < .1f);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
Loading…
Reference in a new issue