Merge origin/master into sensor-catalog-r2 (code; catalog is master's)
Conflicts: EvasionTermTests keeps the branch's EvLonginusDrive split with master's generic design names; AetherDb Program.cs keeps both the sensor-stat-set and generic-designs cases and lists both in the usage line. GameData/Aetheria.cc is taken from origin/master unchanged; sensor-stat-set apply is rerun on it in the next commit. Tests that read the sensor catalog (TheCatalogCarriesSensorStats and the roles checks) do not pass at this commit: it builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
005e2fc57b
103 changed files with 5392 additions and 668 deletions
|
|
@ -46,13 +46,15 @@ public static class Program
|
|||
case "smoke-rounds": return SmokeRoundsCatalog(args.Contains("apply"));
|
||||
case "roles-backfill": return RolesBackfill(args.Contains("apply"));
|
||||
case "consumables-first-set": return ConsumablesFirstSetCatalog(args.Contains("apply"));
|
||||
case "launch-suppressor": return LaunchSuppressorCatalog(args.Contains("apply"));
|
||||
case "pirates-faction":
|
||||
Console.OutputEncoding = new System.Text.UTF8Encoding(false);
|
||||
return PiratesFaction(args.Contains("apply"));
|
||||
case "verse-grammar": return VerseGrammarCommand.Run(args.Contains("apply"));
|
||||
case "sensor-stat-set": return SensorStatSet(args.Contains("apply"));
|
||||
case "generic-designs": return GenericDesigns(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], smoke-hull [apply], smoke-rounds [apply], roles-backfill [apply], pd-gear [apply], consumables-first-set [apply], pirates-faction [apply], verse-grammar [apply], sensor-stat-set [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], smoke-hull [apply], smoke-rounds [apply], roles-backfill [apply], pd-gear [apply], consumables-first-set [apply], launch-suppressor [apply], pirates-faction [apply], verse-grammar [apply], generic-designs [apply], sensor-stat-set [apply]");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -234,7 +236,7 @@ public static class Program
|
|||
{
|
||||
["EnergyDraw"] = "heating element", ["Heat"] = "heating element",
|
||||
},
|
||||
["PotaT+-"] = new Dictionary<string, string>
|
||||
["Capacitor"] = new Dictionary<string, string>
|
||||
{
|
||||
["Capacity"] = "storage cell", ["Efficiency"] = "converter",
|
||||
},
|
||||
|
|
@ -407,7 +409,7 @@ public static class Program
|
|||
private static (float Mean, float Dev, string Basis) AuthorProductRole(AetherDb db, FactionProductData product, string designName, string role)
|
||||
{
|
||||
var maker = db.Cache.Get(product.Manufacturer)?.Name;
|
||||
if (designName == "Arctica" && maker == "Alakrita" && role == "fins")
|
||||
if (designName == "Adaptive Radiator" && maker == "Alakrita" && role == "fins")
|
||||
return (.78f, .07f, "lore: Alakrita brands on heat efficiency (item-provenance-target.md)");
|
||||
|
||||
var hash = StableHash($"{product.Name}|{role}");
|
||||
|
|
@ -1176,10 +1178,10 @@ public static class Program
|
|||
var newDesigns = new (GearData Design, string ProductName, Faction Maker, string ProductDescription, (string Role, float Mean, float Dev)[] Roles)[]
|
||||
{
|
||||
(targetingComputer, "LockOn", FactionByShortName("NiteLife"),
|
||||
"Keeps the lights on your target. (new text)",
|
||||
"Keeps the lights on your target.",
|
||||
new[] { ("processor", .55f, .15f), ("array", .5f, .16f) }),
|
||||
(fireControlArray, "LockOn Pro", FactionByShortName("NiteLife"),
|
||||
"Same promise, sharper lock. (new text)",
|
||||
"Same promise, sharper lock.",
|
||||
new[] { ("processor", .72f, .12f), ("array", .68f, .13f) }),
|
||||
};
|
||||
|
||||
|
|
@ -1192,17 +1194,17 @@ public static class Program
|
|||
// outside this cut's brief).
|
||||
var existingDesigns = new (string DesignName, string ProductName, Faction Maker, string ProductDescription, (string Role, float Mean, float Dev)[] Roles)[]
|
||||
{
|
||||
("plight", "DragOnBreath", FactionByShortName("Lucent"),
|
||||
"The subsidiary plight's own description already namechecks: DragOnBreath, a Lucent Media property.",
|
||||
("Lightning Projector", "DragOnBreath", FactionByShortName("Lucent"),
|
||||
"Chain lightning has your enemies chain smoking while you enjoy a relaxing DragOnBreath vapepop, now available in 17! flavours (manufactured by DragOnBreath, a subsidary of Lucent Media)",
|
||||
new[] { ("focusing array", .7f, .13f), ("power coupling", .55f, .15f) }),
|
||||
("pswarm", "Leonid", FactionByShortName("AU"),
|
||||
("Swarm Rocket Pod", "Leonid", FactionByShortName("AU"),
|
||||
"A rain of pain that falls mainly on space planes.",
|
||||
new[] { ("guidance system", .65f, .14f), ("thruster", .63f, .15f), ("warhead", .7f, .13f) }),
|
||||
("Core Power", "Steadfast", FactionByShortName("R&D"),
|
||||
"Rated for continuous operation. \"Continuous\" does not imply \"uninterrupted.\" (new text)",
|
||||
("Compact Reactor", "Steadfast", FactionByShortName("R&D"),
|
||||
"It gets the job done, but that's all. Rated for continuous operation. \"Continuous\" does not imply \"uninterrupted.\"",
|
||||
new[] { ("core", .5f, .12f), ("regulator", .5f, .12f) }),
|
||||
("Large Drive", "True North", FactionByShortName("Lightsail"),
|
||||
"Gets your cargo there. Getting you there is your problem. (new text)",
|
||||
"Gets your cargo there. Getting you there is your problem.",
|
||||
Array.Empty<(string, float, float)>()),
|
||||
};
|
||||
|
||||
|
|
@ -1314,16 +1316,16 @@ public static class Program
|
|||
var newProducts = new (GearData Design, string ProductName, Faction Maker, string ProductDescription, (string Role, float Mean, float Dev)[] Roles)[]
|
||||
{
|
||||
(targetingComputer, "Panopticon", FactionByShortName("Finch"),
|
||||
"Sees everything, judges nothing. Well, mostly nothing. (new text)",
|
||||
"Sees everything, judges nothing. Well, mostly nothing.",
|
||||
new[] { ("processor", .5f, .13f), ("array", .58f, .12f) }),
|
||||
(fireControlArray, "Panopticon Prime", FactionByShortName("Finch"),
|
||||
"The upgrade nobody at Finch will admit they needed until they had it. (new text)",
|
||||
"The upgrade nobody at Finch will admit they needed until they had it.",
|
||||
new[] { ("processor", .68f, .11f), ("array", .75f, .10f) }),
|
||||
(targetingComputer, "ClapBack", FactionByShortName("Lucent"),
|
||||
"Your target won't clock it until the killcam. (new text)",
|
||||
"Your target won't clock it until the killcam.",
|
||||
new[] { ("processor", .6f, .16f), ("array", .48f, .15f) }),
|
||||
(fireControlArray, "ClapBack Ultra", FactionByShortName("Lucent"),
|
||||
"Now with main-character energy built in. (new text)",
|
||||
"Now with main-character energy built in.",
|
||||
new[] { ("processor", .78f, .14f), ("array", .65f, .13f) }),
|
||||
};
|
||||
|
||||
|
|
@ -1529,7 +1531,7 @@ public static class Program
|
|||
static void Set(PerformanceStat stat, float min, float max) { stat.Min = min; stat.Max = max; }
|
||||
static void Track(WeaponData data, float rate) => data.Tracking = new PerformanceStat { Min = rate, Max = rate, Terms = new List<StatTerm>() };
|
||||
|
||||
var gun = Copy(Template("ClearPath"), designNames[0], "Close-in defence gun: a hose of light rounds.", 40f, 80000, 60f, 1, 1, WeaponModifiers.RapidFire);
|
||||
var gun = Copy(Template("Light Machine Gun"), designNames[0], "Close-in defence gun: a hose of light rounds.", 40f, 80000, 60f, 1, 1, WeaponModifiers.RapidFire);
|
||||
var gunData = gun.Behaviors.OfType<InstantWeaponData>().Single();
|
||||
Set(gunData.Damage, 1.5f, 4f); Set(gunData.Range, 300f, 550f); Set(gunData.MinRange, 0f, 0f);
|
||||
Set(gunData.Cooldown, .06f, .025f); Set(gunData.Count, 1f, 1f); Set(gunData.Spread, 1f, .3f);
|
||||
|
|
@ -1537,14 +1539,14 @@ public static class Program
|
|||
gunData.AmmoType = db.Cache.RefOf(ammunition); gunData.MagazineSize = 250; gunData.ReloadTime = 3f;
|
||||
Track(gunData, 40f);
|
||||
|
||||
var laser = Copy(Template("Spectra"), designNames[1], "Close-in defence beam: quick, light, instant.", 30f, 110000, 30f, 1, 1, Template("Spectra").WeaponModifiers);
|
||||
var laser = Copy(Template("Light Laser"), designNames[1], "Close-in defence beam: quick, light, instant.", 30f, 110000, 30f, 1, 1, Template("Light Laser").WeaponModifiers);
|
||||
var laserData = laser.Behaviors.OfType<InstantWeaponData>().Single();
|
||||
Set(laserData.Damage, 5f, 12f); Set(laserData.Range, 400f, 750f); Set(laserData.MinRange, 0f, 0f);
|
||||
Set(laserData.Cooldown, .2f, .1f); Set(laserData.Count, 1f, 1f); Set(laserData.Spread, .05f, 0f);
|
||||
Set(laserData.Velocity, 0f, 0f); Set(laserData.Energy, 6f, 10f); Set(laserData.Heat, 300f, 150f); Set(laserData.Visibility, 300f, 100f);
|
||||
Track(laserData, 40f);
|
||||
|
||||
var flak = Copy(Template("DeathCluster"), designNames[2], "Small flak gun: a short-range pellet volley.", 120f, 180000, 80f, 1, 2, WeaponModifiers.Cluster);
|
||||
var flak = Copy(Template("Cluster Cannon"), designNames[2], "Small flak gun: a short-range pellet volley.", 120f, 180000, 80f, 1, 2, WeaponModifiers.Cluster);
|
||||
var flakData = flak.Behaviors.OfType<InstantWeaponData>().Single();
|
||||
Set(flakData.Damage, 30f, 70f); Set(flakData.Count, 6f, 8f); Set(flakData.Range, 350f, 800f); Set(flakData.MinRange, 0f, 0f);
|
||||
Set(flakData.Cooldown, .6f, .35f); Set(flakData.Spread, 4f, 2f); Set(flakData.Velocity, 900f, 1300f);
|
||||
|
|
@ -1694,10 +1696,10 @@ public static class Program
|
|||
// 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."),
|
||||
("Sensor Array", "Floodlight", "Lucent", "Sees everything and tells everything. Lucent never saw the point of subtle."),
|
||||
("Sensor Array", "Pinion", "Finch", "A listening ear with a whisper of a voice. Finch sensors are heard last and heard least."),
|
||||
("Compact Sensor", "Beacon", "Lucent", "If it can see you, you can see it. That is called fairness."),
|
||||
("Compact Sensor", "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)
|
||||
|
|
@ -1881,11 +1883,11 @@ public static class Program
|
|||
}
|
||||
static PerformanceStat Fixed(float value) => new PerformanceStat { Min = value, Max = value, Terms = new List<StatTerm>() };
|
||||
|
||||
var proximity = Copy("Spectra", names[0], "A Spectra that bursts by proximity, blast 8. For scenarios; no one builds it.", WeaponFuse.Proximity, 8f);
|
||||
var delayed = Copy("Spectra", names[1], "A Spectra that bursts after two cells of metal, blast 4. For scenarios; no one builds it.", WeaponFuse.Delayed, 4f);
|
||||
var refused = Copy("Spectra", names[2], "A Spectra whose blast is wider than its reach, so it never fires. For scenarios; no one builds it.", WeaponFuse.Proximity, 30f);
|
||||
var charge = Copy("ChargeBlast SG", names[4], "A ChargeBlast SG whose blast is wider than its reach, so it never charges. For scenarios; no one builds it.", WeaponFuse.Proximity, 30f);
|
||||
var beam = Copy("Spectra", names[3], "A constant beam whose blast is wider than its reach, so it never starts. For scenarios; no one builds it.", WeaponFuse.Proximity, 30f);
|
||||
var proximity = Copy("Light Laser", names[0], "A Light Laser that bursts by proximity, blast 8. For scenarios; no one builds it.", WeaponFuse.Proximity, 8f);
|
||||
var delayed = Copy("Light Laser", names[1], "A Light Laser that bursts after two cells of metal, blast 4. For scenarios; no one builds it.", WeaponFuse.Delayed, 4f);
|
||||
var refused = Copy("Light Laser", names[2], "A Light Laser whose blast is wider than its reach, so it never fires. For scenarios; no one builds it.", WeaponFuse.Proximity, 30f);
|
||||
var charge = Copy("Charged Scatter Blaster", names[4], "A Charged Scatter Blaster whose blast is wider than its reach, so it never charges. For scenarios; no one builds it.", WeaponFuse.Proximity, 30f);
|
||||
var beam = Copy("Light Laser", names[3], "A constant beam whose blast is wider than its reach, so it never starts. For scenarios; no one builds it.", WeaponFuse.Proximity, 30f);
|
||||
|
||||
// The Smoke Test Hull carries one 2x2 reactor. A Spectra's 20-40 energy a shot at its cadence is more than that reactor makes,
|
||||
// so two of them overload it, it overheats and shuts down within a second, and nothing aboard fires again (Fused Rounds,
|
||||
|
|
@ -2055,8 +2057,8 @@ public static class Program
|
|||
// no non-flat stat reads; DeathCluster, Flak Gun, GT 3K, plight and the two MoveOnPros because each had a non-flat stat
|
||||
// whose Quality term named no role (CB-R1: each non-flat stat reads exactly one declared role). Idempotent: a design with
|
||||
// no flat mapped stat and no unnamed one 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",
|
||||
"DeathCluster", "Flak Gun", "GT 3K", "plight", "MoveOnPro", "MoveOnPro Station Reactor" };
|
||||
private static readonly string[] BackfillDesigns = { "Autocannon", "Long-Range Missile Rack", "Short-Range Missile Rack", "Mine Launcher", "Large Drive", "Small Drive",
|
||||
"Cluster Cannon", "Flak Gun", "Guided Missile Launcher", "Lightning Projector", "Muon Fusion Reactor", "Muon Fusion Station Reactor" };
|
||||
|
||||
// Stats the backfill names but never ranges: a shot count is whole, and which way a launcher's MinRange should fall is
|
||||
// each design's own call (GT 3K authors it as a cost already).
|
||||
|
|
@ -2220,13 +2222,13 @@ public static class Program
|
|||
var rebrands = new (string Design, string Name, string Description)[]
|
||||
{
|
||||
("Autocannon", "\U0001FAF3\U0001F52B", "Serial number filed off. Still shoots."),
|
||||
("Earp", "\U0001FAA6\U0001F920", "The previous owner had no further use for it."),
|
||||
("FastBlast+-", "\U0001F9FE\U0001F6AB", "Warranty void where prohibited, which is everywhere."),
|
||||
("6k Shooter", "\U0001F3A8\U0001F648", "The logo underneath is somebody else's problem."),
|
||||
("scorched void policy", "\U0001F525", "Turns out we did start the fire after all."),
|
||||
("Long Slug Rifle", "\U0001FAA6\U0001F920", "The previous owner had no further use for it."),
|
||||
("Cell Blaster", "\U0001F9FE\U0001F6AB", "Warranty void where prohibited, which is everywhere."),
|
||||
("Slug Rifle", "\U0001F3A8\U0001F648", "The logo underneath is somebody else's problem."),
|
||||
("Incendiary Rocket Pod", "\U0001F525", "Turns out we did start the fire after all."),
|
||||
("Targeting Computer", "\U0001F440\U0001F3AF", "Somebody else's eyes. They still work."),
|
||||
("Small Drive", "\U0001F511\U0001F3C3\U0001F4A8", "Hotwired. Do not ask about the ignition."),
|
||||
("Store-All Plus", "\U0001F4E6\U0001F92B", "Contents not as declared."),
|
||||
("Cargo Bay", "\U0001F4E6\U0001F92B", "Contents not as declared."),
|
||||
};
|
||||
|
||||
var products = rebrands.Select(r =>
|
||||
|
|
@ -2415,4 +2417,371 @@ public static class Program
|
|||
return 0;
|
||||
}
|
||||
|
||||
// generic-designs: the rename, brand and product-text tables (map: "Generic design names", "Content: brands, flavour and
|
||||
// functional text"). Public so the command test reads the same rows the command writes.
|
||||
public const string AuthoringNote = " (new text)";
|
||||
|
||||
public static readonly (string Old, string Generic, string Functional)[] Renames =
|
||||
{
|
||||
("ClearPath", "Light Machine Gun", "Light ballistic hose: many small rounds at short range."),
|
||||
("6k Shooter", "Slug Rifle", "Single heavy slugs, fired as fast as the trigger."),
|
||||
("Earp", "Long Slug Rifle", "A lighter slug rifle with the longest ballistic reach."),
|
||||
("DeathCluster", "Cluster Cannon", "Medium shotgun: a heavy volley of pellets."),
|
||||
("pretty pretty bang bang", "Charged Scattergun", "Ballistic shotgun that charges for a heavier volley."),
|
||||
("Spectra", "Light Laser", "The lightest beam: low damage, quick cycle, long reach."),
|
||||
("FastBlast+-", "Cell Blaster", "Rapid energy bursts fed by cells that cut its reactor draw."),
|
||||
("ColdFire", "Long Laser", "Heavy small beam: slow, hard-hitting, long reach."),
|
||||
("CShot RainbowLite Lazer", "Heavy Beam Laser", "The heaviest beam: nine cells, enormous damage and reach."),
|
||||
("ChargeBlast+-", "Charged Blaster", "One charged knockout shot."),
|
||||
("ChargeBlast SG", "Charged Scatter Blaster", "Charged energy shotgun."),
|
||||
("plight", "Lightning Projector", "Charged chain lightning."),
|
||||
("GT 3K", "Guided Missile Launcher", "Fires guided missiles."),
|
||||
("LRMM72", "Long-Range Missile Rack", "Large missile rack for long engagements."),
|
||||
("SRMM72", "Short-Range Missile Rack", "Extra-large rack of short-range missiles."),
|
||||
("pswarm", "Swarm Rocket Pod", "Dumbfire rockets in a swarm."),
|
||||
("scorched void policy", "Incendiary Rocket Pod", "Incendiary dumbfire rockets."),
|
||||
("Arctica", "Adaptive Radiator", "Radiator whose output scales widely with conditions."),
|
||||
("Iapyx", "Radiator", "Standard two-cell radiator."),
|
||||
("Skiron", "Long Radiator", "Four-cell radiator with a large surface."),
|
||||
("OK Disperser", "Heat Pump", "Moves heat to where it can be shed."),
|
||||
("cold like my heart", "Cryogenic Heat Pump", "Heavy heat pump that drives temperatures far down."),
|
||||
("Core Power", "Compact Reactor", "Small reactor."),
|
||||
("Manhattan", "Heavy Reactor", "Large three-by-three reactor."),
|
||||
("Notorious", "High-Yield Reactor", "Large reactor with a wide output range."),
|
||||
("Vulcan", "Ring Reactor", "Ring-shaped reactor, four by four."),
|
||||
("MoveOnPro", "Muon Fusion Reactor", "Compact fusion reactor that draws a reservoir."),
|
||||
("Core Power Station Reactor", "Station Reactor", "A reactor sized for a station's core."),
|
||||
("Manhattan Station Reactor", "Heavy Station Reactor", "A heavy reactor sized for a station's core."),
|
||||
("Notorious Station Reactor", "High-Yield Station Reactor", "A high-yield reactor sized for a station's core."),
|
||||
("Vulcan Station Reactor", "Ring Station Reactor", "A ring reactor sized for a station's core."),
|
||||
("MoveOnPro Station Reactor", "Muon Fusion Station Reactor", "A muon fusion reactor sized for a station's core."),
|
||||
("RevvITup 2.0", "High-Thrust Drive", "Large drive: huge thrust, little torque."),
|
||||
("deep space burnout", "Burn Drive", "Large drive that runs very hot."),
|
||||
("Talaria", "Rugged Drive", "Two-cell working drive."),
|
||||
("Victoire", "Racing Drive", "Feather-light two-cell drive with extreme response."),
|
||||
("The Bat", "Sensor Array", "Two-by-two long-range sensor array."),
|
||||
("not if i see you first", "Compact Sensor", "One-cell sensor."),
|
||||
("PotaT+-", "Capacitor", "Stores charge for burst draw."),
|
||||
("SafeStuff II", "Secure Cargo Bay", "Three-by-three secured hold."),
|
||||
("Store-All Enterprise Edition", "Large Cargo Bay", "Six-by-six hold."),
|
||||
("Store-All Plus", "Cargo Bay", "Two-by-two hold."),
|
||||
("Store-All Plus Mini", "Small Cargo Bay", "One-cell hold."),
|
||||
};
|
||||
|
||||
public static readonly (string Design, string Maker, string OldProduct, string Brand, string Text, string DesignText)[] Brands =
|
||||
{
|
||||
("Autocannon", "Zhestokost", "Autocannon", "Hammerfall",
|
||||
"The rounds fall steadily, and they do not stop until the work is done.",
|
||||
"Belt-fed cannon: a steady stream of rounds at long reach."),
|
||||
("Cockpit 2x2", "Lightsail", "Cockpit 2x2", "Wheelhouse",
|
||||
"Every Lightsail captain's favourite chair. The view comes standard.",
|
||||
"Two-by-two crew cockpit."),
|
||||
("Industrial Thermostatic Heater", "Lightsail", "Industrial Thermostatic Heater", "Hearth",
|
||||
"A warm hold, a warm crew, and a warm welcome at the cold end of the run.",
|
||||
"Turns reactor power into heat to keep the hull warm."),
|
||||
("Medium Drive", "Lightsail", "Medium Drive", "Steady Haul",
|
||||
"Keeps you going and going and going...",
|
||||
"Mid-sized working drive."),
|
||||
("Small Drive", "Lightsail", "Small Drive", "Short Hop",
|
||||
"Dock to dock, and not a metre further than the manifest says.",
|
||||
"Small working drive."),
|
||||
("Turret Control Module", "AU", "Turret Control Module", "Watchtower",
|
||||
"Point it at the dark and get back to work. It keeps watch.",
|
||||
"Control module for a station turret."),
|
||||
("Medium Docking Bay", "AU", "Medium Docking Bay", "Open Berth",
|
||||
"Room for a friend, a customer or a tug. We don't ask which.",
|
||||
"Docking bay for mid-sized ships."),
|
||||
("Mine Launcher", "DME", "Mine Launcher", "parting gift",
|
||||
"we were never here/ but we left you something/ it's waiting where we were",
|
||||
"Lays a drifting proximity mine that arms after two seconds."),
|
||||
};
|
||||
|
||||
public static readonly (string Product, string Maker, string Text)[] ProductTexts =
|
||||
{
|
||||
("Victoire", "Alakrita",
|
||||
"The most responsive thruster on the market, for the most irresponsible of racing pilots."),
|
||||
("DragOnBreath", "Lucent",
|
||||
"Chain lightning has your enemies chain smoking while you enjoy a relaxing DragOnBreath vapepop, now available in 17! flavours (manufactured by DragOnBreath, a subsidary of Lucent Media)"),
|
||||
("Steadfast", "R&D",
|
||||
"It gets the job done, but that's all. Rated for continuous operation. \"Continuous\" does not imply \"uninterrupted.\""),
|
||||
("Steadfast Station", "R&D",
|
||||
"Rated for continuous operation at station scale. The fine print scales too."),
|
||||
("Manhattan Station", "Zhestokost",
|
||||
"A fortress needs a furnace. This is the furnace."),
|
||||
("Notorious Station", "Lucent",
|
||||
"The biggest reaction in the system, and everyone docked here will hear about it."),
|
||||
("Vulcan Station", "AU",
|
||||
"Keeps the lights on and the forges hot."),
|
||||
("MoveOnPro Station", "NiteLife",
|
||||
"The club never closes. Neither does the reactor."),
|
||||
("LRMM72", "R&D",
|
||||
"Engages targets at up to seventy-two kilometres!* *In a vacuum, against a cooperative target."),
|
||||
("SRMM72", "DME",
|
||||
"seventy-two reasons/ to keep your distance/ all of them leaving at once"),
|
||||
("Iapyx", "AU",
|
||||
"Two cells of honest fin. Bolt it on and forget it's there."),
|
||||
("Skiron", "AU",
|
||||
"When the job runs hot, run a bigger fin."),
|
||||
("Talaria", "AU",
|
||||
"Takes a knock, keeps pushing. Built for crews who work for a living."),
|
||||
("RevvITup 2.0", "NiteLife",
|
||||
"Floor it. The bass drops when you do."),
|
||||
("PotaT+-", "NiteLife",
|
||||
"Holds the charge until the drop, then lets it all go at once."),
|
||||
("not if i see you first", "DME",
|
||||
"eyes in the dark/ you were seen before you looked/ say goodnight"),
|
||||
("Space Tractor", "Ewan Hart",
|
||||
"It's a tractor. In space. Trap your enemies in gravity wells. Haul thousands of tons of hay through space!"),
|
||||
};
|
||||
|
||||
// Designs whose text was flavour or named a brand, rewritten as tech-node text without a rename (ruling designs-are-tech-nodes).
|
||||
// The Large Drive's old line ships as Steady Haul's text; the Tractor Beam's moves to its product, Space Tractor (ProductTexts).
|
||||
public static readonly (string Design, string Text)[] DesignTexts =
|
||||
{
|
||||
("Large Drive", "Large working drive."),
|
||||
("Tractor Beam", "Holds a target in a gravity well and hauls it in."),
|
||||
("Smoke Proximity", "A Light Laser that bursts by proximity, blast 8. For scenarios; no one builds it."),
|
||||
("Smoke Delayed", "A Light Laser that bursts after two cells of metal, blast 4. For scenarios; no one builds it."),
|
||||
("Smoke Refused", "A Light Laser whose blast is wider than its reach, so it never fires. For scenarios; no one builds it."),
|
||||
("Smoke Refused Charge", "A Charged Scatter Blaster whose blast is wider than its reach, so it never charges. For scenarios; no one builds it."),
|
||||
};
|
||||
|
||||
// generic-designs: a design is named and described like a node on a tech tree, and the brand lives in the product it
|
||||
// sells (rulings catalog-grows-generic-designs-branded-products, kitchen-sink-content, designs-are-tech-nodes). A
|
||||
// Renames row renames the design found under its old name and gives it the functional text; a Brands row renames the
|
||||
// maker's same-named product and sets its text and the design's functional text; ProductTexts sets the rest; every
|
||||
// shipped product text ending in the authoring note loses it. Dry run unless passed "apply".
|
||||
public static int GenericDesigns(bool apply, string root = null)
|
||||
{
|
||||
var db = AetherDb.Open(catalogWritable: apply, root: root);
|
||||
var designs = db.Cache.GetAll<EquippableItemData>().ToArray();
|
||||
var products = db.Cache.GetAll<FactionProductData>().ToArray();
|
||||
string MakerOf(FactionProductData product) => db.Cache.Get(product.Manufacturer)?.ShortName;
|
||||
var changed = new List<(object Document, CultRecordKey Key)>();
|
||||
var report = new List<string>();
|
||||
|
||||
void Write(object document, string what, string before, string after)
|
||||
{
|
||||
if (!changed.Any(c => ReferenceEquals(c.Document, document))) changed.Add((document, db.Cache.RefOf(document).Key));
|
||||
report.Add($"{what}: \"{before}\" -> \"{after}\"");
|
||||
}
|
||||
|
||||
foreach (var (old, generic, functional) in Renames)
|
||||
{
|
||||
var found = designs.Where(d => d.Name == old).ToArray();
|
||||
var held = designs.Where(d => d.Name == generic).ToArray();
|
||||
if (found.Length == 0 && held.Length == 0) throw new InvalidOperationException($"Rename row \"{old}\" -> \"{generic}\": no design carries either name.");
|
||||
if (found.Length == 0) continue;
|
||||
if (held.Length != 0) throw new InvalidOperationException($"Rename row \"{old}\" -> \"{generic}\": the generic name is already held by another design.");
|
||||
if (found.Length != 1) throw new InvalidOperationException($"Rename row \"{old}\" -> \"{generic}\": {found.Length} designs carry the old name.");
|
||||
var design = found[0];
|
||||
Write(design, "design name", design.Name, generic);
|
||||
design.Name = generic;
|
||||
if (design.Description != functional) Write(design, $"design \"{generic}\" text", design.Description, functional);
|
||||
design.Description = functional;
|
||||
}
|
||||
|
||||
foreach (var (designName, maker, oldProduct, brand, text, designText) in Brands)
|
||||
{
|
||||
var found = designs.Where(d => d.Name == designName).ToArray();
|
||||
if (found.Length != 1) throw new InvalidOperationException($"Brand row \"{brand}\": {found.Length} designs are named \"{designName}\".");
|
||||
var design = found[0];
|
||||
var key = db.Cache.RefOf(design).Key;
|
||||
var sold = products.Where(p => p.Design.Key.Equals(key) && MakerOf(p) == maker).ToArray();
|
||||
var product = sold.FirstOrDefault(p => p.Name == brand) ?? sold.FirstOrDefault(p => p.Name == oldProduct);
|
||||
if (product == null) throw new InvalidOperationException($"Brand row \"{brand}\": {maker} sells no product of \"{designName}\" under either name.");
|
||||
if (product.Name == brand) continue;
|
||||
if (products.Any(p => !ReferenceEquals(p, product) && p.Name == brand)) throw new InvalidOperationException($"Brand row \"{brand}\": the brand is already held by another product.");
|
||||
Write(product, $"product of \"{designName}\" by {maker}", product.Name, brand);
|
||||
product.Name = brand;
|
||||
Write(product, $"product \"{brand}\" text", product.Description, text);
|
||||
product.Description = text;
|
||||
if (design.Description != designText) Write(design, $"design \"{designName}\" text", design.Description, designText);
|
||||
design.Description = designText;
|
||||
}
|
||||
|
||||
foreach (var (designName, text) in DesignTexts)
|
||||
{
|
||||
var found = designs.Where(d => d.Name == designName).ToArray();
|
||||
if (found.Length != 1) throw new InvalidOperationException($"Design text row \"{designName}\": {found.Length} designs carry the name.");
|
||||
if (found[0].Description == text) continue;
|
||||
Write(found[0], $"design \"{designName}\" text", found[0].Description, text);
|
||||
found[0].Description = text;
|
||||
}
|
||||
|
||||
foreach (var (name, maker, text) in ProductTexts)
|
||||
{
|
||||
var found = products.Where(p => p.Name == name && MakerOf(p) == maker).ToArray();
|
||||
if (found.Length != 1) throw new InvalidOperationException($"Product text row \"{name}\" by {maker}: {found.Length} products match.");
|
||||
if (found[0].Description == text) continue;
|
||||
Write(found[0], $"product \"{name}\" text", found[0].Description, text);
|
||||
found[0].Description = text;
|
||||
}
|
||||
|
||||
foreach (var product in products.Where(p => p.Description != null && p.Description.EndsWith(AuthoringNote, StringComparison.Ordinal)))
|
||||
{
|
||||
var stripped = product.Description.Substring(0, product.Description.Length - AuthoringNote.Length);
|
||||
Write(product, $"product \"{product.Name}\" authoring note stripped", product.Description, stripped);
|
||||
product.Description = stripped;
|
||||
}
|
||||
|
||||
if (changed.Count == 0)
|
||||
{
|
||||
Console.WriteLine("Already applied; nothing to do.");
|
||||
return 0;
|
||||
}
|
||||
foreach (var line in report) Console.WriteLine(line);
|
||||
if (!apply)
|
||||
{
|
||||
Console.WriteLine($"\nDry run. Pass \"apply\" to write {changed.Count} records.");
|
||||
return 0;
|
||||
}
|
||||
foreach (var (document, _) in changed) CultRecordRefs.Validate(document);
|
||||
// ExpectUnchanged lands the batch onto the file as it is: every record the command did not author keeps the bytes it has,
|
||||
// where a plain commit would re-encode the whole catalog through today's types.
|
||||
if (!db.Cache.Commit(batch =>
|
||||
{
|
||||
batch.ExpectUnchanged();
|
||||
foreach (var (document, key) in changed) batch.Upsert(document.GetType(), document, key);
|
||||
}))
|
||||
throw new InvalidOperationException("The catalog changed while the generic designs were being authored; nothing was written. Run it again.");
|
||||
Console.WriteLine($"\nWrote {changed.Count} records to Aetheria.cc");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// aetheria-ew cut ew-hide-launch-catalog (rulings ew-suppressor-strength-ruled, ew-suppressor-makers-ruled,
|
||||
// ew-mild-suppressor-makers-defaulted): two launch suppressors on the Tool hardpoint, one helper for both. The strong
|
||||
// Launch Suppressor hides a launch from its own target and gives out after an opening volley; the mild Launch Baffle
|
||||
// only dims the flash for distant observers and runs through a long fight. Every figure is one Quality term, linear in
|
||||
// its role (baffles for the Factor, heat sink for the heat), so quality moves the part and nothing else does. Thermal
|
||||
// constants and temperatures come from the Targeting Computer, the catalog's 1x1 tool, as the consumables do. Dry run
|
||||
// unless passed "apply".
|
||||
public static int LaunchSuppressorCatalog(bool apply, string root = null)
|
||||
{
|
||||
var db = AetherDb.Open(catalogWritable: apply, root: root);
|
||||
var designNames = new[] { "Launch Suppressor", "Launch Baffle" };
|
||||
var template = db.Cache.GetAll<GearData>().Single(g => g.Name == "Targeting Computer");
|
||||
|
||||
Faction Maker(string shortName) =>
|
||||
db.Cache.GetAll<Faction>().FirstOrDefault(f => f.ShortName == shortName) ?? throw new InvalidOperationException($"No faction with short name \"{shortName}\".");
|
||||
PerformanceStat Rolled(float min, float max, string role) => new PerformanceStat
|
||||
{
|
||||
Min = min,
|
||||
Max = max,
|
||||
Terms = new List<StatTerm> { new StatTerm { Source = StatSource.Quality, Exponent = 1f, Role = role } },
|
||||
};
|
||||
// Min applies at quality 0 of the role and Max at quality 1: a falling Factor and falling heat per unit are Min > Max.
|
||||
GearData Design(string name, string description, int price, (float Min, float Max) factor, (float Min, float Max) heatPerUnit, (float Min, float Max) rated)
|
||||
{
|
||||
var design = new GearData
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Hardpoint = HardpointType.Tool,
|
||||
Mass = 60f,
|
||||
Price = price,
|
||||
Durability = 50f,
|
||||
Shape = new Shape(1, 1),
|
||||
SpecificHeat = template.SpecificHeat,
|
||||
Conductivity = template.Conductivity,
|
||||
MinimumTemperature = template.MinimumTemperature,
|
||||
MaximumTemperature = template.MaximumTemperature,
|
||||
OptimalTemperature = template.OptimalTemperature,
|
||||
PlateauWidth = template.PlateauWidth,
|
||||
ThermalResilience = template.ThermalResilience,
|
||||
Roles = new[] { "baffles", "heat sink" }.Select(r => new ItemRole { Name = r }).ToList(),
|
||||
Behaviors = new List<BehaviorData>
|
||||
{
|
||||
new LaunchSuppressorData
|
||||
{
|
||||
Factor = Rolled(factor.Min, factor.Max, "baffles"),
|
||||
HeatPerUnit = Rolled(heatPerUnit.Min, heatPerUnit.Max, "heat sink"),
|
||||
RatedTemperature = Rolled(rated.Min, rated.Max, "heat sink"),
|
||||
},
|
||||
},
|
||||
};
|
||||
design.Shape.Cells[0, 0] = true;
|
||||
return design;
|
||||
}
|
||||
|
||||
var strong = Design(designNames[0], "Baffles and a heat sink remove nearly all of a weapon's launch flash, hiding the shot from its target. The removed light is dissipated as heat in the device's own cell; above its rated temperature it passes the full flash.",
|
||||
40000, (.08f, .03f), (1.4f, .8f), (310f, 330f));
|
||||
var mild = Design(designNames[1], "Baffles dim a weapon's launch flash to a fraction and a heat sink absorbs the difference. The target still sees the shot; distant observers do not. The sink sustains the effect through a long fight.",
|
||||
25000, (.4f, .2f), (.8f, .5f), (310f, 330f));
|
||||
var designs = new[] { strong, mild };
|
||||
|
||||
var products = new (GearData Design, string Name, string Maker, string Description)[]
|
||||
{
|
||||
(strong, "Velvet Rope", "NiteLife", "Keeps the flash on the inside. Nobody sees who you let out."),
|
||||
(strong, "Hush", "Finch", "Small, quiet, and the first shot is a secret. The second, probably."),
|
||||
(strong, "lights out", "DME", "no flash/ no name/ nobody saw a thing"),
|
||||
(strong, "Plausible Deniability", "R&D", "Hides 100% of launches!* *Launches defined as the ones nobody saw."),
|
||||
(mild, "Low Profile", "Lightsail", "Quiet on the manifest, quiet on the scope. A hauler nobody notices is a hauler that gets paid."),
|
||||
(mild, "Fast Pass Dim", "Alakrita", "Takes the glare off every launch and keeps the schedule. Anyone close enough to mind was already your problem."),
|
||||
};
|
||||
foreach (var product in products) Maker(product.Maker);
|
||||
|
||||
// Authored when both designs exist and every product is present on its own design; untouched when nothing is
|
||||
// present; anything between needs a human, a product name that belongs to an unrelated item included.
|
||||
var existing = designNames.Select(name => db.Cache.GetAll<GearData>().SingleOrDefault(g => g.Name == name)).ToArray();
|
||||
var productNames = products.Select(p => p.Name).ToHashSet();
|
||||
var presentProducts = db.Cache.GetAll<FactionProductData>().Where(p => productNames.Contains(p.Name)).ToArray();
|
||||
if (existing.All(d => d != null) && presentProducts.Length == products.Length
|
||||
&& products.All(p => presentProducts.Any(x => x.Name == p.Name && x.Design.Key.Equals(db.Cache.RefOf(existing[p.Design == strong ? 0 : 1]).Key))))
|
||||
{
|
||||
Console.WriteLine("The launch suppressors are already authored; nothing to do.");
|
||||
return 0;
|
||||
}
|
||||
if (existing.Any(d => d != null) || presentProducts.Length != 0)
|
||||
throw new InvalidOperationException("The launch suppressors are partly authored; the catalog needs a human.");
|
||||
|
||||
Console.WriteLine("New designs:");
|
||||
foreach (var design in designs)
|
||||
{
|
||||
var data = design.Behaviors.OfType<LaunchSuppressorData>().Single();
|
||||
Console.WriteLine($" {design.Name,-18} {design.Shape.Width}x{design.Shape.Height} {design.Hardpoint} mass {design.Mass:0.##} price {design.Price} durability {design.Durability:0.##} roles [{string.Join(", ", design.Roles.Select(r => r.Name))}]");
|
||||
foreach (var (label, stat) in new[] { ("Factor", data.Factor), ("HeatPerUnit", data.HeatPerUnit), ("RatedTemperature", data.RatedTemperature) })
|
||||
Console.WriteLine($" {label} {stat.Min:0.##}-{stat.Max:0.##} ({stat.Terms.Single().Role})");
|
||||
Console.WriteLine($" \"{design.Description}\"");
|
||||
}
|
||||
Console.WriteLine("\nNew products:");
|
||||
foreach (var (design, name, maker, description) in products)
|
||||
{
|
||||
var (mean, dev) = MakerProfiles[maker];
|
||||
Console.WriteLine($" {design.Name} -> \"{name}\" by {maker}, every role mean {mean:0.##} dev {dev:0.##}: {description}");
|
||||
}
|
||||
|
||||
if (!apply)
|
||||
{
|
||||
Console.WriteLine($"\nDry run. Pass \"apply\" to author 2 designs and {products.Length} products.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (var design in designs) CultRecordRefs.Validate(design);
|
||||
// ExpectUnchanged lands the batch onto the file as it is: every other record keeps the bytes it has, where a plain
|
||||
// commit would re-encode the whole catalog through today's types.
|
||||
if (!db.Cache.Commit(batch =>
|
||||
{
|
||||
batch.ExpectUnchanged();
|
||||
var keys = new Dictionary<GearData, CultRecordKey>();
|
||||
foreach (var design in designs) keys[design] = batch.Upsert(typeof(GearData), design);
|
||||
foreach (var (design, name, maker, description) in products)
|
||||
{
|
||||
var (mean, dev) = MakerProfiles[maker];
|
||||
batch.Upsert(typeof(FactionProductData), new FactionProductData
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Design = new CultRecordRef<CraftedItemData>(keys[design]),
|
||||
Manufacturer = db.Cache.RefOf(Maker(maker)),
|
||||
Roles = design.Roles.Select(r => new ProductRole { Role = r.Name, Mean = mean, StandardDeviation = dev }).ToList(),
|
||||
});
|
||||
}
|
||||
}))
|
||||
throw new InvalidOperationException("The catalog changed while the launch suppressors were being authored; nothing was written. Run it again.");
|
||||
Console.WriteLine($"\nAuthored 2 designs and {products.Length} products in Aetheria.cc");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue