Roles backfill: AetherDb command and catalog role tests (CB-R1)
AetherDb roles-backfill gives Autocannon, LRMM72, SRMM72, Mine Launcher, Large Drive (and Small Drive, which declared roles no stat read) live roles: each mapped flat stat becomes a 0.8x..1.2x range read through a Quality term naming its role, and every seller authors a ProductRole per role from its maker profile. MineLayerRoles is new; the maker profile table is hoisted so pd-gear and this command share it. ShippedCatalogRolesAreLive and RolesBackfillIsAuthored read the shipped catalog. The catalog blob lands in its own commit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e389e3972c
commit
54999a642b
2 changed files with 265 additions and 10 deletions
126
tests/Aetheria.Shared.Tests/CatalogRoleTests.cs
Normal file
126
tests/Aetheria.Shared.Tests/CatalogRoleTests.cs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
// Aetheria release, cut roles-backfill, rule CB-R1: a crafted design declares roles iff a stat varies with quality,
|
||||
// every declared role is read by a non-flat stat, and every seller authors one ProductRole per role. Both tests read
|
||||
// the shipped catalog through the real reader.
|
||||
public sealed class CatalogRoleTests
|
||||
{
|
||||
// Every weapon and every gear of a kind that carries per-part quality stats (the kinds roles-migrate keeps a role
|
||||
// map for). Cargo bays, docking bays, control modules, tools and hulls carry no quality-reading stat.
|
||||
private static readonly HardpointType[] QualityGearKinds =
|
||||
{ HardpointType.Radiator, HardpointType.Reactor, HardpointType.Sensors, HardpointType.Thruster };
|
||||
|
||||
private static IEnumerable<PerformanceStat> StatsOf(EquippableItemData design) =>
|
||||
design.Behaviors.Where(b => b != null)
|
||||
.SelectMany(b => b.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat)).Select(f => (PerformanceStat)f.GetValue(b)))
|
||||
.Where(s => s?.Terms != null);
|
||||
|
||||
private static PerformanceStat Stat(EquippableItemData design, string field) =>
|
||||
Assert.Single(design.Behaviors.SelectMany(b => b.GetType().GetFields().Where(f => f.Name == field && f.FieldType == typeof(PerformanceStat)).Select(f => (PerformanceStat)f.GetValue(b))));
|
||||
|
||||
private static string[] NamedQualityRoles(PerformanceStat stat) =>
|
||||
stat.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => t.Role).ToArray();
|
||||
|
||||
[Fact]
|
||||
public void ShippedCatalogRolesAreLive()
|
||||
{
|
||||
var cache = RestoredHullsTests.OpenCatalog();
|
||||
var products = cache.GetAll<FactionProductData>().ToArray();
|
||||
var broken = new List<string>();
|
||||
foreach (var design in cache.GetAll<EquippableItemData>()
|
||||
.Where(i => i is WeaponItemData || (i is GearData && QualityGearKinds.Contains(i.HardpointType)))
|
||||
.OrderBy(i => i.Name, StringComparer.Ordinal))
|
||||
{
|
||||
var varying = StatsOf(design).Where(s => s.Min != s.Max).ToArray();
|
||||
var read = varying.SelectMany(NamedQualityRoles).Distinct().OrderBy(r => r, StringComparer.Ordinal).ToArray();
|
||||
var declared = (design.Roles ?? new List<ItemRole>()).Select(r => r.Name).OrderBy(r => r, StringComparer.Ordinal).ToArray();
|
||||
if (varying.Length == 0) broken.Add($"{design.Name}: every stat is flat");
|
||||
if (!read.SequenceEqual(declared))
|
||||
broken.Add($"{design.Name}: declares [{string.Join(", ", declared)}] but non-flat stats read [{string.Join(", ", read)}]");
|
||||
foreach (var product in products.Where(p => p.Design.Key.Equals(cache.RefOf(design).Key)))
|
||||
{
|
||||
var authored = (product.Roles ?? new List<ProductRole>()).Select(r => r.Role).OrderBy(r => r, StringComparer.Ordinal).ToArray();
|
||||
if (!authored.SequenceEqual(declared))
|
||||
broken.Add($"{design.Name}: product \"{product.Name}\" authors [{string.Join(", ", authored)}], not each of [{string.Join(", ", declared)}] once");
|
||||
}
|
||||
}
|
||||
Assert.True(broken.Count == 0, string.Join("\n", broken));
|
||||
}
|
||||
|
||||
// value, then the stat's expected Min and Max: a benefit rises with quality (0.8x to 1.2x), a cost falls (1.2x to 0.8x).
|
||||
private static void AssertRange(EquippableItemData design, string field, float value, bool cost, string role)
|
||||
{
|
||||
var stat = Stat(design, field);
|
||||
Assert.Equal(value * (cost ? 1.2f : .8f), stat.Min, 3);
|
||||
Assert.Equal(value * (cost ? .8f : 1.2f), stat.Max, 3);
|
||||
Assert.Contains(role, NamedQualityRoles(stat));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RolesBackfillIsAuthored()
|
||||
{
|
||||
var cache = RestoredHullsTests.OpenCatalog();
|
||||
EquippableItemData Design(string name) => Assert.Single(cache.GetAll<EquippableItemData>(), i => i.Name == name);
|
||||
string[] Roles(EquippableItemData d) => d.Roles.Select(r => r.Name).OrderBy(r => r, StringComparer.Ordinal).ToArray();
|
||||
|
||||
var autocannon = Design("Autocannon");
|
||||
Assert.Equal(new[] { "barrel", "feed mechanism" }, Roles(autocannon));
|
||||
AssertRange(autocannon, "Damage", 3f, false, "barrel");
|
||||
AssertRange(autocannon, "Range", 1000f, false, "barrel");
|
||||
|
||||
var lrm = Design("LRMM72");
|
||||
Assert.Equal(new[] { "guidance system", "thruster", "warhead" }, Roles(lrm));
|
||||
AssertRange(lrm, "Damage", 256f, false, "warhead");
|
||||
AssertRange(lrm, "Cooldown", 10f, true, "guidance system");
|
||||
|
||||
var srm = Design("SRMM72");
|
||||
Assert.Equal(new[] { "guidance system", "thruster", "warhead" }, Roles(srm));
|
||||
AssertRange(srm, "Damage", 128f, false, "warhead");
|
||||
AssertRange(srm, "Cooldown", 5f, true, "guidance system");
|
||||
|
||||
var mine = Design("Mine Launcher");
|
||||
Assert.Equal(new[] { "dispenser", "warhead" }, Roles(mine));
|
||||
AssertRange(mine, "Damage", 50f, false, "warhead");
|
||||
AssertRange(mine, "Cooldown", 2f, true, "dispenser");
|
||||
AssertRange(mine, "Spread", 5f, true, "dispenser");
|
||||
var layer = Assert.Single(mine.Behaviors.OfType<MineLayerData>());
|
||||
Assert.Equal(2f, layer.ArmingDelay);
|
||||
Assert.Equal(2f, layer.FuseDelay);
|
||||
Assert.Equal(30f, layer.Lifetime);
|
||||
|
||||
var drive = Design("Large Drive");
|
||||
Assert.Equal(new[] { "injector", "nozzle" }, Roles(drive));
|
||||
AssertRange(drive, "Thrust", 250000f, false, "nozzle");
|
||||
AssertRange(drive, "Visibility", 100f, true, "nozzle");
|
||||
AssertRange(drive, "Heat", 2500f, true, "injector");
|
||||
|
||||
var smallDrive = Design("Small Drive");
|
||||
AssertRange(smallDrive, "Thrust", 75000f, false, "nozzle");
|
||||
|
||||
// Each seller's means come from its maker's profile; the Pirates are middling and inconsistent.
|
||||
var profiles = new Dictionary<string, (float Mean, float Dev)>
|
||||
{
|
||||
["Zhestokost"] = (.50f, .08f), ["R&D"] = (.60f, .15f), ["DME"] = (.55f, .20f), ["Lightsail"] = (.55f, .10f), ["Pirates"] = (.45f, .22f),
|
||||
};
|
||||
foreach (var design in new[] { autocannon, lrm, srm, mine, drive })
|
||||
{
|
||||
var sellers = cache.GetAll<FactionProductData>().Where(p => p.Design.Key.Equals(cache.RefOf(design).Key)).ToArray();
|
||||
Assert.NotEmpty(sellers);
|
||||
foreach (var product in sellers)
|
||||
{
|
||||
var maker = cache.Get(product.Manufacturer);
|
||||
var (mean, dev) = profiles[maker.ShortName];
|
||||
Assert.Equal(Roles(design), product.Roles.Select(r => r.Role).OrderBy(r => r, StringComparer.Ordinal).ToArray());
|
||||
Assert.All(product.Roles, r =>
|
||||
{
|
||||
Assert.Equal(mean, r.Mean, 3);
|
||||
Assert.Equal(dev, r.StandardDeviation, 3);
|
||||
});
|
||||
}
|
||||
}
|
||||
Assert.Contains(cache.GetAll<FactionProductData>(), p => p.Design.Key.Equals(cache.RefOf(autocannon).Key) && cache.Get(p.Manufacturer).ShortName == "Pirates");
|
||||
}
|
||||
}
|
||||
|
|
@ -42,11 +42,12 @@ public static class Program
|
|||
case "field-kinds": return FieldKindsCatalog(args.Contains("apply"));
|
||||
case "pd-gear": return PdGearCatalog(args.Contains("apply"));
|
||||
case "mine-launcher": return MineLauncherCatalog(args.Contains("apply"));
|
||||
case "roles-backfill": return RolesBackfill(args.Contains("apply"));
|
||||
case "pirates-faction":
|
||||
Console.OutputEncoding = new System.Text.UTF8Encoding(false);
|
||||
return PiratesFaction(args.Contains("apply"));
|
||||
default:
|
||||
Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear <Type.Member>]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], mine-launcher [apply], pd-gear [apply], pirates-faction [apply]");
|
||||
Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear <Type.Member>]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], mine-launcher [apply], roles-backfill [apply], pd-gear [apply], pirates-faction [apply]");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -182,6 +183,15 @@ public static class Program
|
|||
["Thrust"] = "thruster", ["MissileVelocity"] = "thruster", ["Velocity"] = "thruster",
|
||||
};
|
||||
|
||||
// Warhead and dispenser: a mine layer is a launcher without guidance or thrust, so its stats split between the
|
||||
// charge it lays and the mechanism that lays it. ArmingDelay, FuseDelay and Lifetime are timings, not stats.
|
||||
private static readonly Dictionary<string, string> MineLayerRoles = new Dictionary<string, string>
|
||||
{
|
||||
["Damage"] = "warhead", ["Penetration"] = "warhead", ["DamageSpread"] = "warhead",
|
||||
["Range"] = "dispenser", ["Cooldown"] = "dispenser", ["Spread"] = "dispenser", ["Velocity"] = "dispenser",
|
||||
["Energy"] = "dispenser", ["Heat"] = "dispenser", ["Visibility"] = "dispenser",
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, string> RadiatorRoles = new Dictionary<string, string>
|
||||
{
|
||||
["Emissivity"] = "fins", ["WasteHeat"] = "fins",
|
||||
|
|
@ -231,6 +241,17 @@ public static class Program
|
|||
},
|
||||
};
|
||||
|
||||
// One Mean / StandardDeviation per maker, applied to every role of a design the maker sells (catalog-growth
|
||||
// addenda, "Maker profiles"); the Pirates are middling and inconsistent (pirates-record).
|
||||
private static readonly Dictionary<string, (float Mean, float Dev)> MakerProfiles = new Dictionary<string, (float Mean, float Dev)>
|
||||
{
|
||||
["Zhestokost"] = (.50f, .08f), ["AU"] = (.50f, .12f), ["Lightsail"] = (.55f, .10f), ["R&D"] = (.60f, .15f),
|
||||
["Lucent"] = (.60f, .15f), ["NiteLife"] = (.55f, .12f), ["Finch"] = (.65f, .20f), ["Alakrita"] = (.60f, .14f),
|
||||
["DME"] = (.55f, .20f), ["Adrasteia"] = (.60f, .10f),
|
||||
};
|
||||
|
||||
private static readonly (float Mean, float Dev) PirateProfile = (.45f, .22f);
|
||||
|
||||
private static string KindOf(EquippableItemData item) =>
|
||||
item is HullData hull ? $"Hull/{hull.HullType}"
|
||||
: item is DockingBayData ? "DockingBay"
|
||||
|
|
@ -1527,12 +1548,6 @@ public static class Program
|
|||
flakData.AmmoType = db.Cache.RefOf(ammunition); flakData.MagazineSize = 40; flakData.ReloadTime = 4f;
|
||||
Track(flakData, 20f);
|
||||
|
||||
var makers = new Dictionary<string, (float Mean, float Dev)>
|
||||
{
|
||||
["Zhestokost"] = (.50f, .08f), ["AU"] = (.50f, .12f), ["Lightsail"] = (.55f, .10f), ["R&D"] = (.60f, .15f),
|
||||
["Lucent"] = (.60f, .15f), ["NiteLife"] = (.55f, .12f), ["Finch"] = (.65f, .20f), ["Alakrita"] = (.60f, .14f),
|
||||
["DME"] = (.55f, .20f), ["Adrasteia"] = (.60f, .10f),
|
||||
};
|
||||
var products = new (WeaponItemData Design, string Name, string Maker, string Description)[]
|
||||
{
|
||||
(gun, "Waykeeper", "Lightsail", "Freight first, questions later. Keeps a hauler's paint unscratched through a full salvo."),
|
||||
|
|
@ -1563,7 +1578,7 @@ public static class Program
|
|||
Console.WriteLine("\nNew products:");
|
||||
foreach (var (design, name, maker, description) in products)
|
||||
{
|
||||
var (mean, dev) = makers[maker];
|
||||
var (mean, dev) = MakerProfiles[maker];
|
||||
Console.WriteLine($" {design.Name} -> \"{name}\" by {maker}, every role mean {mean:0.##} dev {dev:0.##}: {description}");
|
||||
}
|
||||
|
||||
|
|
@ -1580,7 +1595,7 @@ public static class Program
|
|||
foreach (var design in new[] { gun, laser, flak }) keys[design] = batch.Upsert(typeof(WeaponItemData), design);
|
||||
foreach (var (design, name, maker, description) in products)
|
||||
{
|
||||
var (mean, dev) = makers[maker];
|
||||
var (mean, dev) = MakerProfiles[maker];
|
||||
batch.Upsert(typeof(FactionProductData), new FactionProductData
|
||||
{
|
||||
Name = name,
|
||||
|
|
@ -1706,6 +1721,120 @@ public static class Program
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Aetheria release, cut roles-backfill (rule CB-R1): designs that shipped with flat stats and no roles get the
|
||||
// roles their kind's map names, and every seller authors a ProductRole per role. Each mapped stat becomes a range
|
||||
// of value x (0.8, 1.2) that rises with quality for a benefit and falls for a cost, read through one Quality term
|
||||
// naming its role (exponent: the mode of the kind's existing designs on that field, 1 when there is none; an
|
||||
// existing Quality term keeps its exponent). Zero stats stay flat. Small Drive joined the set because it declares
|
||||
// roles that no non-flat stat reads. Idempotent: a design with no flat mapped stat is left alone, a product with a
|
||||
// role is left alone. Dry run unless passed "apply".
|
||||
private static readonly string[] BackfillDesigns = { "Autocannon", "LRMM72", "SRMM72", "Mine Launcher", "Large Drive", "Small Drive" };
|
||||
|
||||
private static readonly HashSet<string> CostStats = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"Cooldown", "Spread", "Heat", "Visibility", "Energy", "EnergyUsage", "ChargeTime", "ChargeEnergy", "ChargeHeat",
|
||||
"SensorImpact", "DirectionImpact", "Decay",
|
||||
};
|
||||
|
||||
private static Dictionary<string, string> BackfillMapFor(EquippableItemData item) =>
|
||||
item.Behaviors.OfType<MineLayerData>().Any() ? MineLayerRoles : RoleMapFor(item, KindOf(item));
|
||||
|
||||
private static IEnumerable<PerformanceStat> StatsOf(EquippableItemData item) =>
|
||||
(item.Behaviors ?? new List<BehaviorData>()).Where(b => b != null)
|
||||
.SelectMany(b => b.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat)).Select(f => f.GetValue(b) as PerformanceStat))
|
||||
.Where(s => s?.Terms != null);
|
||||
|
||||
private static int RolesBackfill(bool apply)
|
||||
{
|
||||
var db = AetherDb.Open(catalogWritable: apply);
|
||||
var items = db.Cache.GetAll<EquippableItemData>().ToArray();
|
||||
var products = db.Cache.GetAll<FactionProductData>().ToArray();
|
||||
var targets = BackfillDesigns.Select(name => items.Where(i => i.Name == name).ToArray())
|
||||
.Select((found, i) => found.Length == 1 ? found[0] : throw new InvalidOperationException(
|
||||
$"Expected exactly one design named \"{BackfillDesigns[i]}\", found {found.Length}.")).ToArray();
|
||||
|
||||
float ExponentFor(string kind, string field)
|
||||
{
|
||||
var precedent = items.Except(targets).Where(i => KindOf(i) == kind && i.Roles != null && i.Roles.Count > 0)
|
||||
.SelectMany(i => i.Behaviors ?? new List<BehaviorData>()).Where(b => b != null)
|
||||
.SelectMany(b => b.GetType().GetFields().Where(f => f.Name == field && f.FieldType == typeof(PerformanceStat)).Select(f => f.GetValue(b) as PerformanceStat))
|
||||
.Where(s => s?.Terms != null && s.Min != s.Max)
|
||||
.SelectMany(s => s.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => t.Exponent))
|
||||
.GroupBy(e => e).OrderByDescending(g => g.Count()).ThenBy(g => g.Key).Select(g => g.Key).ToArray();
|
||||
return precedent.Length > 0 ? precedent[0] : 1f;
|
||||
}
|
||||
|
||||
var changedDesigns = new List<(EquippableItemData Document, CultRecordKey Key)>();
|
||||
foreach (var design in targets)
|
||||
{
|
||||
var kind = KindOf(design);
|
||||
var map = BackfillMapFor(design) ?? throw new InvalidOperationException($"\"{design.Name}\" has no role map.");
|
||||
var lines = new List<string>();
|
||||
foreach (var behavior in design.Behaviors.Where(b => b != null))
|
||||
foreach (var field in behavior.GetType().GetFields().Where(f => f.FieldType == typeof(PerformanceStat)))
|
||||
{
|
||||
if (!map.TryGetValue(field.Name, out var role) || !(field.GetValue(behavior) is PerformanceStat stat)) continue;
|
||||
if (stat.Min != stat.Max || stat.Min == 0f || float.IsInfinity(stat.Min)) continue;
|
||||
var value = stat.Min;
|
||||
(stat.Min, stat.Max) = CostStats.Contains(field.Name) ? (value * 1.2f, value * .8f) : (value * .8f, value * 1.2f);
|
||||
stat.Terms ??= new List<StatTerm>();
|
||||
var quality = stat.Terms.FirstOrDefault(t => t.Source == StatSource.Quality);
|
||||
if (quality == null)
|
||||
{
|
||||
quality = new StatTerm { Source = StatSource.Quality, Exponent = ExponentFor(kind, field.Name) };
|
||||
stat.Terms.Add(quality);
|
||||
}
|
||||
quality.Role = role;
|
||||
lines.Add($" {behavior.GetType().Name}.{field.Name,-16} {value:0.####} -> {stat.Min:0.####}..{stat.Max:0.####} {role} ^{quality.Exponent:0.##}");
|
||||
}
|
||||
if (lines.Count == 0) continue;
|
||||
design.Roles = StatsOf(design).Where(s => s.Min != s.Max)
|
||||
.SelectMany(s => s.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => t.Role))
|
||||
.Distinct().OrderBy(r => r, StringComparer.Ordinal).Select(r => new ItemRole { Name = r }).ToList();
|
||||
Console.WriteLine($" {design.Name} ({kind}) roles [{string.Join(", ", design.Roles.Select(r => r.Name))}]");
|
||||
foreach (var line in lines) Console.WriteLine(line);
|
||||
changedDesigns.Add((design, db.Cache.RefOf(design).Key));
|
||||
}
|
||||
|
||||
var changedProducts = new List<(FactionProductData Document, CultRecordKey Key)>();
|
||||
foreach (var design in targets)
|
||||
foreach (var product in products.Where(p => p.Design.Key.Equals(db.Cache.RefOf(design).Key)).OrderBy(p => p.Name, StringComparer.Ordinal))
|
||||
{
|
||||
var maker = db.Cache.Get(product.Manufacturer);
|
||||
var profile = maker?.Name == "Pirates" ? PirateProfile
|
||||
: maker != null && MakerProfiles.TryGetValue(maker.ShortName, out var found) ? found
|
||||
: throw new InvalidOperationException($"No maker profile for the seller of \"{design.Name}\" ({maker?.ShortName ?? "no maker"}).");
|
||||
product.Roles ??= new List<ProductRole>();
|
||||
var missing = design.Roles.Select(r => r.Name).Where(r => product.Roles.All(p => p.Role != r)).ToArray();
|
||||
if (missing.Length == 0) continue;
|
||||
foreach (var role in missing)
|
||||
product.Roles.Add(new ProductRole { Role = role, Mean = profile.Mean, StandardDeviation = profile.Dev });
|
||||
Console.WriteLine($" {product.Name,-28} ({design.Name}, {maker.ShortName}) {string.Join(", ", missing)} at {profile.Mean:0.00}/{profile.Dev:0.00}");
|
||||
changedProducts.Add((product, db.Cache.RefOf(product).Key));
|
||||
}
|
||||
|
||||
if (changedDesigns.Count == 0 && changedProducts.Count == 0)
|
||||
{
|
||||
Console.WriteLine("Roles backfill is already authored; nothing to do.");
|
||||
return 0;
|
||||
}
|
||||
Console.WriteLine($"\n{changedDesigns.Count} design(s) and {changedProducts.Count} product(s) to author.");
|
||||
if (!apply)
|
||||
{
|
||||
Console.WriteLine("Dry run. Pass \"apply\" to land them.");
|
||||
return 0;
|
||||
}
|
||||
foreach (var (document, _) in changedDesigns) CultRecordRefs.Validate(document);
|
||||
foreach (var (document, _) in changedProducts) CultRecordRefs.Validate(document);
|
||||
db.Cache.Commit(batch =>
|
||||
{
|
||||
foreach (var (document, key) in changedDesigns) batch.Upsert(document is WeaponItemData ? typeof(WeaponItemData) : typeof(GearData), document, key);
|
||||
foreach (var (document, key) in changedProducts) batch.Upsert(typeof(FactionProductData), document, key);
|
||||
});
|
||||
Console.WriteLine($"Landed {changedDesigns.Count} design(s) and {changedProducts.Count} product(s) in Aetheria.cc");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Aetheria release, cut pirates-record (rulings pirates-record-content, catalog-grows-generic-designs-branded-products):
|
||||
// the Pirates faction and eight rebrands of other makers' designs, named in emoji. The Pirates own no design: their range
|
||||
// is what they took, so every product points at a design someone else also makes. Each product carries one ProductRole per
|
||||
|
|
@ -1765,7 +1894,7 @@ public static class Program
|
|||
{
|
||||
var design = DesignByName(r.Design);
|
||||
return (Design: design, r.Name, r.Description,
|
||||
Roles: (design.Roles ?? new List<ItemRole>()).Select(role => new ProductRole { Role = role.Name, Mean = .45f, StandardDeviation = .22f }).ToList());
|
||||
Roles: (design.Roles ?? new List<ItemRole>()).Select(role => new ProductRole { Role = role.Name, Mean = PirateProfile.Mean, StandardDeviation = PirateProfile.Dev }).ToList());
|
||||
}).ToArray();
|
||||
|
||||
Console.WriteLine($"New faction \"{pirates.Name}\": influence {pirates.InfluenceDistance}, names \"{nameFile.Name}\", allegiance 1.0 to {pirates.Allegiance.Count} factions.");
|
||||
|
|
|
|||
Loading…
Reference in a new issue