sensor-stat-set: Lucent and Finch products for the two sensor designs; catalog test

The catalog sold only two sensor designs (DME and Miss Terri's), so brand flavour had
nothing to vary: the command adds one Lucent and one Finch product per design. Adds
TheCatalogCarriesSensorStats. Catalog not yet regenerated; not built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-13 19:15:19 +02:00
commit bce2cd0154
2 changed files with 77 additions and 7 deletions

View file

@ -182,4 +182,40 @@ public sealed class CatalogRoleTests
}
Assert.Contains(cache.GetAll<FactionProductData>(), p => p.Design.Key.Equals(cache.RefOf(autocannon).Key) && cache.Get(p.Manufacturer).ShortName == "Pirates");
}
// aetheria-release cut sensor-catalog (ruling sensor-stat-set): every sensor design's intercept factor varies with its
// maker's emission-discipline quality, brand flavour is a product's quality (Lucent loud, Finch quiet), and the
// Passive Array is a Tool listener with two products from makers other than Miss Terri's and the Pirates.
[Fact]
public void TheCatalogCarriesSensorStats()
{
using var cache = RestoredHullsTests.OpenCatalog();
var products = cache.GetAll<FactionProductData>().ToArray();
string MakerOf(FactionProductData p) => cache.Get(p.Manufacturer).ShortName;
var sensors = cache.GetAll<EquippableItemData>().Where(d => d.Behaviors.OfType<SensorData>().Any()).ToArray();
Assert.NotEmpty(sensors);
foreach (var design in sensors)
{
var alpha = design.Behaviors.OfType<SensorData>().Single().InterceptFactor;
Assert.NotNull(alpha);
Assert.NotEqual(alpha.Min, alpha.Max);
Assert.Equal(new[] { "emission-discipline" }, NamedQualityRoles(alpha));
}
double[] Discipline(string maker) => products
.Where(p => sensors.Any(s => cache.RefOf(s).Key.Equals(p.Design.Key)) && MakerOf(p) == maker)
.Select(p => (double)p.Roles.Single(r => r.Role == "emission-discipline").Mean).ToArray();
var lucent = Discipline("Lucent");
var finch = Discipline("Finch");
Assert.NotEmpty(lucent);
Assert.NotEmpty(finch);
Assert.True(lucent.Max() < finch.Min(), "a Lucent sensor must be louder than every Finch one");
var array = Assert.Single(cache.GetAll<EquippableItemData>(), d => d.Name == "Passive Array");
Assert.Equal(HardpointType.Tool, array.HardpointType);
Assert.True(array.Behaviors.OfType<ListenerData>().Single().Multiple.Min >= 2f);
var makers = products.Where(p => p.Design.Key.Equals(cache.RefOf(array).Key)).Select(MakerOf).ToArray();
Assert.Equal(2, makers.Distinct().Count());
Assert.DoesNotContain(makers, m => m == "Pirates" || m == "Miss Terri's");
}
}

View file

@ -1619,12 +1619,14 @@ public static class Program
// The intercept factor alpha is how far others hear a sensor against how far it sees: a quality-ranged stat from
// .5 (loud) to 2 (quiet) whose midpoint quality .5 reads 1, so lerp(Min, Max, q^e) with e = log2(3). Brand flavour
// lives in the product's emission-discipline quality (ruling sensor-stat-set): Lucent is low, Finch and Sol high,
// every other maker middling. A maker with no profile of its own (the Pirates) takes the Pirate deviation.
// every other maker middling. A maker with no profile of its own (Miss Terri's, the Pirates) keeps the deviation its
// product already authored for the emitter role.
private static float DisciplineMean(Faction maker) =>
maker.ShortName == "Lucent" ? .2f : maker.ShortName == "Finch" || maker.ShortName == "Sol" || maker.Name == "Sol" ? .8f : .5f;
private static float MakerDeviation(Faction maker) =>
MakerProfiles.TryGetValue(maker.ShortName ?? "", out var profile) ? profile.Dev : PirateProfile.Dev;
private static float MakerDeviation(Faction maker, FactionProductData product) =>
MakerProfiles.TryGetValue(maker.ShortName ?? "", out var profile) ? profile.Dev
: product.Roles.FirstOrDefault(r => r.Role == "emitter")?.StandardDeviation ?? throw new InvalidOperationException($"Product \"{product.Name}\" has no emitter spread to take a deviation from.");
// aetheria-release cut sensor-catalog (rulings sensor-stat-set, catalog-grows-generic-designs-branded-products): every
// SensorData design's InterceptFactor becomes a quality-ranged stat reading the emission-discipline role, every
@ -1677,7 +1679,7 @@ public static class Program
var maker = MakerOf(product) ?? throw new InvalidOperationException($"Product \"{product.Name}\" has no maker.");
product.Roles ??= new List<ProductRole>();
var mean = DisciplineMean(maker);
var dev = MakerDeviation(maker);
var dev = MakerDeviation(maker, product);
var existing = product.Roles.FirstOrDefault(r => r.Role == EmissionDiscipline);
if (existing != null && existing.Mean == mean && existing.StandardDeviation == dev) continue;
if (existing != null) { existing.Mean = mean; existing.StandardDeviation = dev; }
@ -1686,6 +1688,25 @@ public static class Program
}
}
// The catalog sells two sensor designs, one branded by DME and one by Miss Terri's, and no Lucent or Finch sensor at all,
// so the brand flavour has nothing to vary across: each design gains a loud Lucent and a quiet Finch product
// (ruling catalog-grows-generic-designs-branded-products). Every role of the design is authored at the maker's profile,
// emission-discipline at the maker's flavour.
var sensorProducts = new (string Design, string Name, string Maker, string Description)[]
{
("The Bat", "Floodlight", "Lucent", "Sees everything and tells everything. Lucent never saw the point of subtle."),
("The Bat", "Pinion", "Finch", "A listening ear with a whisper of a voice. Finch sensors are heard last and heard least."),
("not if i see you first", "Beacon", "Lucent", "If it can see you, you can see it. That is called fairness."),
("not if i see you first", "Kestrel", "Finch", "Hangs in the dark and looks down. The prey rarely notices."),
};
var newProducts = new List<(EquippableItemData Design, string Name, Faction Maker, string Description)>();
foreach (var (designName, name, makerName, description) in sensorProducts)
{
if (products.Any(p => p.Name == name)) continue;
var design = sensors.Single(s => s.Name == designName);
newProducts.Add((design, name, Maker(makerName), description));
}
// The Passive Array takes its thermal envelope and sound from a sensor gear, then becomes a Tool listener.
GearData array = null;
var arrayProducts = new List<(string Name, string Maker, string Description)>();
@ -1712,8 +1733,8 @@ public static class Program
Console.WriteLine($"\nNew design: Passive Array [Tool] 1x1, EnergyDraw 10/s, Multiple 2-3 reading receiver; products {string.Join(", ", arrayProducts.Select(p => $"{p.Name} by {p.Maker}"))}");
}
Console.WriteLine($"\nIntercept factor: {changedDesigns.Count} designs and {changedProducts.Count} products to change; Passive Array {(array == null ? "already authored" : "to author")}.");
if (changedDesigns.Count == 0 && changedProducts.Count == 0 && array == null)
Console.WriteLine($"\nIntercept factor: {changedDesigns.Count} designs and {changedProducts.Count} products to change, {newProducts.Count} sensor products to add; Passive Array {(array == null ? "already authored" : "to author")}.");
if (changedDesigns.Count == 0 && changedProducts.Count == 0 && newProducts.Count == 0 && array == null)
{
Console.WriteLine("Sensor stats are already authored; nothing to do.");
return 0;
@ -1728,6 +1749,19 @@ public static class Program
{
foreach (var (document, key) in changedDesigns) batch.Upsert(document.GetType(), document, key);
foreach (var (document, key) in changedProducts) batch.Upsert(document.GetType(), document, key);
foreach (var (design, name, maker, description) in newProducts)
{
var (profileMean, profileDev) = MakerProfiles[maker.ShortName];
batch.Upsert(typeof(FactionProductData), new FactionProductData
{
Name = name, Description = description,
Design = new CultRecordRef<CraftedItemData>(db.Cache.RefOf(design).Key),
Manufacturer = db.Cache.RefOf(maker),
Roles = design.Roles.Select(r => r.Name == EmissionDiscipline
? new ProductRole { Role = r.Name, Mean = DisciplineMean(maker), StandardDeviation = profileDev }
: new ProductRole { Role = r.Name, Mean = profileMean, StandardDeviation = profileDev }).ToList(),
});
}
if (array == null) return;
var arrayKey = batch.Upsert(typeof(GearData), array);
foreach (var (name, maker, description) in arrayProducts)
@ -1742,7 +1776,7 @@ public static class Program
});
}
});
Console.WriteLine($"Landed {changedDesigns.Count} designs and {changedProducts.Count} products{(array == null ? "" : ", the Passive Array and its products")} in Aetheria.cc");
Console.WriteLine($"Landed {changedDesigns.Count} designs, {changedProducts.Count} products changed and {newProducts.Count} added{(array == null ? "" : ", the Passive Array and its products")} in Aetheria.cc");
return 0;
}