From 38198b6e00a7706930b3613c5718aedbe4282c49 Mon Sep 17 00:00:00 2001
From: Meta Vens
Date: Sun, 18 Oct 2026 07:04:15 +0200
Subject: [PATCH] Lot.EffectiveQuality: the item's one quality, the mean of
min(part, assembly) over its fills
QualityForRole's unnamed, roleless and unfilled fallbacks read it. Builds.
Co-Authored-By: Claude Sonnet 5.5
---
Assets/Scripts/ServerShared/Provenance.cs | 20 +++++++---
.../AssemblyFromMakerTests.cs | 38 ++++++++++++++++---
tests/Aetheria.Shared.Tests/LoadoutTests.cs | 4 +-
3 files changed, 49 insertions(+), 13 deletions(-)
diff --git a/Assets/Scripts/ServerShared/Provenance.cs b/Assets/Scripts/ServerShared/Provenance.cs
index adf83c308..4121332d6 100644
--- a/Assets/Scripts/ServerShared/Provenance.cs
+++ b/Assets/Scripts/ServerShared/Provenance.cs
@@ -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
{
diff --git a/tests/Aetheria.Shared.Tests/AssemblyFromMakerTests.cs b/tests/Aetheria.Shared.Tests/AssemblyFromMakerTests.cs
index efc1e1b3a..301a67cd1 100644
--- a/tests/Aetheria.Shared.Tests/AssemblyFromMakerTests.cs
+++ b/tests/Aetheria.Shared.Tests/AssemblyFromMakerTests.cs
@@ -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 { 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 { 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() }.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
+ {
+ 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() }.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.
diff --git a/tests/Aetheria.Shared.Tests/LoadoutTests.cs b/tests/Aetheria.Shared.Tests/LoadoutTests.cs
index 4efbd0b30..9fe71b898 100644
--- a/tests/Aetheria.Shared.Tests/LoadoutTests.cs
+++ b/tests/Aetheria.Shared.Tests/LoadoutTests.cs
@@ -490,7 +490,7 @@ 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.
[Fact]
public void StatsReadTheLot()
{
@@ -525,7 +525,7 @@ public sealed class LoadoutTests : IDisposable
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);