diff --git a/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs b/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs index 9978f081d..7465b567f 100644 --- a/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs +++ b/tests/Aetheria.Shared.Tests/CatalogRoleTests.cs @@ -182,4 +182,40 @@ public sealed class CatalogRoleTests } Assert.Contains(cache.GetAll(), 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().ToArray(); + string MakerOf(FactionProductData p) => cache.Get(p.Manufacturer).ShortName; + var sensors = cache.GetAll().Where(d => d.Behaviors.OfType().Any()).ToArray(); + Assert.NotEmpty(sensors); + foreach (var design in sensors) + { + var alpha = design.Behaviors.OfType().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(), d => d.Name == "Passive Array"); + Assert.Equal(HardpointType.Tool, array.HardpointType); + Assert.True(array.Behaviors.OfType().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"); + } } diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index d292869bf..17b68c528 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -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(); 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(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; }