Briefs, comments and tests name current gear; generic-designs lands only its own records and rewrites the last flavour and brand design texts

Scenario Briefs (Crossing, Duel, FusedRounds, LauncherAngles, StarvedReactor, WeaponFeel), scenario and test comments and test locals say the design names (Lightning Projector, Light Laser, Cell Blaster, Guided Missile Launcher, Swarm Rocket Pod, Muon Fusion Reactor, Cluster Cannon, Charged Scatter Blaster, Rugged Drive, Racing Drive) instead of the retired brand names; Crossing.PlightRange is ProjectorRange.

generic-designs: the batch carries ExpectUnchanged so every record it does not author keeps its bytes (the plain commit re-encoded four records, dropping SensorData key 7). DesignTexts rewrites Large Drive, Tractor Beam, Smoke Proximity, Smoke Delayed, Smoke Refused and Smoke Refused Charge as tech-node text; the Tractor Beam's old line moves to its product Space Tractor. smoke-rounds' authored text names Light Laser and Charged Scatter Blaster to match.

Tests: NoScenarioBriefNamesRetiredGear, TheCapacitorDesignCarriesItsRoleMap, NoDesignTextNamesAProduct, and a command test that widens every payload and checks apply changes only the three records it authors and restores the rewritten texts.

Not built or run at this commit; GameData/Aetheria.cc is regenerated in the next commit, so NoDesignTextNamesAProduct and the command test fail here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-13 22:34:08 +02:00
commit 205c9f5205
20 changed files with 180 additions and 55 deletions

View file

@ -4,8 +4,8 @@ using static ItemRotation;
public sealed class Crossing : Scenario
{
public override string Name => "Crossing";
public override string Brief => "plight at the starboard gun and a FastBlast+- at the port gun, a fast Djinni crossing close ahead and an " +
"unpiloted one waiting at range. The Djinni starts to port moving across the bow; the arena's gravity bends its run. Make the pass coasting: give no throttle until the Djinni has crossed the bow, then fly freely (thrusting from the start carries you past it). Verify: as the Djinni crosses close, FastBlast lands some and plight " +
public override string Brief => "A Lightning Projector at the starboard gun and a Cell Blaster at the port gun, a fast Djinni crossing close ahead and an " +
"unpiloted one waiting at range. The Djinni starts to port moving across the bow; the arena's gravity bends its run. Make the pass coasting: give no throttle until the Djinni has crossed the bow, then fly freely (thrusting from the start carries you past it). Verify: as the Djinni crosses close, FastBlast lands some and projector " +
"almost none; against the Djinni at range both land. Fly it again without the Fire Control Array: " +
"with it the small gun lands more.";
public override uint Seed => 112;
@ -13,11 +13,11 @@ public sealed class Crossing : Scenario
// Where the Djinni sit: the player is at the origin facing +z. The crosser starts ahead and to port, facing across
// the bow and already moving at the Djinni's 50 m/s cap, so its run sweeps across the bow within seconds (the arena
// pulls the player to port, which brings the two together). The waiting Djinni sits inside plight's arc at 80% of
// pulls the player to port, which brings the two together). The waiting Djinni sits inside the Lightning Projector's arc at 80% of
// its Range.
public static readonly CultMath.float2 CrosserAt = float2(-150, 200), CrosserFacing = float2(1, 0), CrosserVelocity = float2(50, 0);
public const float PlightRange = 1216f; // plight's Range in metres, 1216.4 as the fit stands settled at rest
public static readonly CultMath.float2 Waiting = float2(0, PlightRange * .8f);
public const float ProjectorRange = 1216f; // the Lightning Projector's Range in metres, 1216.4 as the fit stands settled at rest
public static readonly CultMath.float2 Waiting = float2(0, ProjectorRange * .8f);
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(DemoTerminus.Cast);

View file

@ -4,7 +4,7 @@ using static ItemRotation;
public sealed class Duel : Scenario
{
public override string Name => "Duel";
public override string Brief => "One piloted, hostile Longinus with slow GT 3K missiles and a Spectra in its hold. Verify: rolled " +
public override string Brief => "One piloted, hostile Longinus with slow guided missiles and a Light Laser in its hold. Verify: rolled " +
"hits and near-misses, subsystem aim, beam reads above bow, armour takes the missile, loot keeps its brand.";
public override uint Seed => 104;
public override bool Ambient => false;

View file

@ -4,20 +4,20 @@ using static ItemRotation;
public sealed class FusedRounds : Scenario
{
public override string Name => "Fused Rounds";
public override string Brief => "The Smoke Test Hull with Smoke Proximity, Smoke Delayed and a DeathCluster; a hostile bare Longinus at mid range, a bare " +
public override string Brief => "The Smoke Test Hull with Smoke Proximity, Smoke Delayed and a Cluster Cannon; a hostile bare Longinus at mid range, a bare " +
"hull dead ahead, a fragile one to starboard, a close one inside the proximity blast radius, and one astern. " +
"Verify with nothing selected: aim past every hull and the round bursts at max range along the aim; aim at the hull " +
"ahead and it stops and bursts on it; fire at the fragile hull and kill it with the DeathCluster before the round " +
"ahead and it stops and bursts on it; fire at the fragile hull and kill it with the Cluster Cannon before the round " +
"lands: no burst. Select the astern hull: the round fires and bursts at its range along the arc edge. Select the " +
"close hull: the burst is pushed out to the blast radius from your centre. A Smoke Proximity burst beside the " +
"Longinus damages where the model shows the blast; a Smoke Delayed round into its nose bursts inside. The HUD " +
"reads Direct, Burst at N m or Refused for the fused weapons and a percentage for the DeathCluster, and the " +
"reads Direct, Burst at N m or Refused for the fused weapons and a percentage for the Cluster Cannon, and the " +
"smoke weapons show no Manufacturer row.";
public override uint Seed => 108;
public override bool Ambient => false;
// Where everything sits: the player is at the origin facing +z. Ahead lies on the initial aim line, inside every forward
// mount's arc. Fragile is a bare Longinus inside the DeathCluster's range; Close is inside Smoke Proximity's blast radius
// mount's arc. Fragile is a bare Longinus inside the Cluster Cannon's range; Close is inside Smoke Proximity's blast radius
// (8 m from the ship's centre) and beside the aim line, not across it; Stern is outside every mount's arc.
public static readonly CultMath.float2 Mid = float2(-200, 600), Ahead = float2(0, 300), Fragile = float2(140, 420),
Close = float2(6.4f, 4f), Stern = float2(0, -400);

View file

@ -4,8 +4,8 @@ using static ItemRotation;
public sealed class LauncherAngles : Scenario
{
public override string Name => "Launcher Angles";
public override string Brief => "A GT 3K, a pswarm and two FastBlasts; a hostile bare Longinus broadside on, another beside it. " +
"Verify: hits pulse only the facing edge; a pswarm round whose target dies fades unexploded.";
public override string Brief => "A Guided Missile Launcher, a Swarm Rocket Pod and two Cell Blasters; a hostile bare Longinus broadside on, another beside it. " +
"Verify: hits pulse only the facing edge; a Swarm Rocket Pod round whose target dies fades unexploded.";
public override uint Seed => 105;
public override bool Ambient => false;

View file

@ -4,7 +4,7 @@ using static ItemRotation;
public sealed class StarvedReactor : Scenario
{
public override string Name => "Starved Reactor";
public override string Brief => "Two Spectras on a MoveOnPro, which can fall to a quarter of Core Power's charge, and a bare hull " +
public override string Brief => "Two Light Lasers on a Muon Fusion Reactor, which can fall to a quarter of the Compact Reactor's charge, and a bare hull " +
"to shoot. Verify: starvation reads as starvation, brownout degrades, refills stutter.";
public override uint Seed => 102;
public override bool Ambient => false;

View file

@ -4,9 +4,9 @@ using static ItemRotation;
public sealed class WeaponFeel : Scenario
{
public override string Name => "Weapon Feel";
public override string Brief => "The Smoke Test Hull with a ChargeBlast SG, an Autocannon and a FastBlast+- burst weapon; a bare Longinus dead ahead. " +
"Verify: ChargeBlast SG and Autocannon fire with effects, particles and action-bar icons; the burst weapon's " +
"cadence is as before; a fast release-and-press on the ChargeBlast SG starts no second charge.";
public override string Brief => "The Smoke Test Hull with a Charged Scatter Blaster, an Autocannon and a Cell Blaster burst weapon; a bare Longinus dead ahead. " +
"Verify: Charged Scatter Blaster and Autocannon fire with effects, particles and action-bar icons; the burst weapon's " +
"cadence is as before; a fast release-and-press on the Charged Scatter Blaster starts no second charge.";
public override uint Seed => 107;
public override bool Ambient => false;

View file

@ -108,6 +108,28 @@ public sealed class CatalogRoleTests
Assert.True(unbranded.Length == 0, "designs sold under their own name: " + string.Join(", ", unbranded));
}
// The Capacitor is the one Tool design with a role map by name; a key that falls out of step with the design's name leaves
// roles-backfill finding no map for it.
[Fact]
public void TheCapacitorDesignCarriesItsRoleMap()
{
using var cache = RestoredHullsTests.OpenCatalog();
var capacitor = cache.GetAll<EquippableItemData>().Single(design => design.Name == "Capacitor");
Assert.True(Program.HasRoleMap(capacitor), "roles-backfill finds no role map for the Capacitor design");
}
// A design is a tech node: its text may not name a product the catalog sells (the brand lives on the product).
[Fact]
public void NoDesignTextNamesAProduct()
{
using var cache = RestoredHullsTests.OpenCatalog();
var productNames = cache.GetAll<FactionProductData>().Select(p => p.Name).Where(n => n.Length > 3).ToHashSet();
var branded = cache.GetAll<EquippableItemData>().Where(design => design is not HullData && design.Description != null)
.Where(design => productNames.Any(n => design.Description.Contains(n)))
.Select(design => design.Name).OrderBy(n => n, StringComparer.Ordinal).ToArray();
Assert.True(branded.Length == 0, "designs whose text names a product: " + string.Join(", ", branded));
}
// Authoring notes are not player text, and every product of a design this cut renamed or branded speaks in its own
// voice (Miss Terri's and the Pirates' records are other passes').
[Fact]

View file

@ -9,8 +9,8 @@ using float3 = CultMath.float3;
// staging what its brief claims fails here.
public sealed partial class RunStartTests
{
// Crossing: the demo's cast; plight and FastBlast+- fitted in separate weapon groups with the Fire Control Array
// aboard; a piloted hostile Djinni that crosses the bow inside 400 m; and an unpiloted neutral one inside plight's
// Crossing: the demo's cast; projector and Cell Blaster fitted in separate weapon groups with the Fire Control Array
// aboard; a piloted hostile Djinni that crosses the bow inside 400 m; and an unpiloted neutral one inside the Lightning Projector's
// arc, short of its Range and beyond three quarters of it.
[Fact]
public void CrossingConditions()
@ -21,12 +21,12 @@ public sealed partial class RunStartTests
Assert.Equal(4, galaxy.HomeZones.Count);
var player = staged.Player;
var plight = player.Equipment.Single(item => item.Data.Name == "Lightning Projector");
var projector = player.Equipment.Single(item => item.Data.Name == "Lightning Projector");
var small = player.Equipment.Single(item => item.Data.Name == "Cell Blaster");
Assert.Contains(player.Equipment, item => item.Data.Name == "Fire Control Array");
var groupOf = new System.Func<EquippedItem, int>(item => System.Array.FindIndex(player.WeaponGroups, group => group.items.Contains(item)));
Assert.True(groupOf(plight) >= 0 && groupOf(small) >= 0, "both guns are in a weapon group");
Assert.NotEqual(groupOf(plight), groupOf(small));
Assert.True(groupOf(projector) >= 0 && groupOf(small) >= 0, "both guns are in a weapon group");
Assert.NotEqual(groupOf(projector), groupOf(small));
Assert.Equal(2, staged.Entities.Count);
var crosser = staged.Entities[0];
@ -54,10 +54,10 @@ public sealed partial class RunStartTests
Assert.Equal(0, AgentsOf(arena, waiting));
var toWaiting = waiting.Position - player.Position;
EvSettle(player); // a weapon's Range is read once the ship's stats have settled; the geometry above is as staged
var range = plight.Behaviors.OfType<Weapon>().Single().Range;
Assert.True(FireControl.InArc(plight, toWaiting), "the waiting Djinni is inside plight's arc");
Assert.True(length(toWaiting) <= range, "the waiting Djinni is inside plight's Range");
Assert.True(length(toWaiting) > .75f * range, "the waiting Djinni is beyond three quarters of plight's Range");
var range = projector.Behaviors.OfType<Weapon>().Single().Range;
Assert.True(FireControl.InArc(projector, toWaiting), "the waiting Djinni is inside the Lightning Projector's arc");
Assert.True(length(toWaiting) <= range, "the waiting Djinni is inside the Lightning Projector's Range");
Assert.True(length(toWaiting) > .75f * range, "the waiting Djinni is beyond three quarters of the Lightning Projector's Range");
// The crossing is what the sim does with that staging, read in the player's frame: the arena's gravity and the
// crosser's pilot act from the first tick, so the t=0 facing proves nothing. Within CrossingSeconds the crosser's
@ -79,8 +79,8 @@ public sealed partial class RunStartTests
crossedMetres = length(after);
Assert.True(after.z > 0, "the crosser passes the bow line ahead of the player, not astern");
Assert.True(length(after) < CrossingMetres, "the crosser passes inside the stated distance");
Assert.True(FireControl.InArc(small, after), "FastBlast+- bears on the crosser as it crosses");
Assert.True(dot(FireControl.MountDirection(small), float3(player.Direction.x, 0, player.Direction.y)) > 0, "FastBlast+- mounts forward");
Assert.True(FireControl.InArc(small, after), "Cell Blaster bears on the crosser as it crosses");
Assert.True(dot(FireControl.MountDirection(small), float3(player.Direction.x, 0, player.Direction.y)) > 0, "Cell Blaster mounts forward");
}
}
Assert.True(swept, $"the crosser's bearing never swept across the bow within {CrossingSeconds} s");

View file

@ -22,7 +22,7 @@ public sealed partial class RunStartTests
{
private const float EvDt = 1f / 60f;
// The Duel's guns on a Longinus: the generated Longinus's thrusters (two Large Drive forward, two Talaria on the
// The Duel's guns on a Longinus: the generated Longinus's thrusters (two Large Drive forward, two Rugged Drive on the
// flanks) and power, its Targeting Computer, and the Duel's cockpit, guns, launchers and sensor.
private static ScenarioFit EvLonginus(ScenarioStage stage, string gun = "Cell Blaster", string second = null) => stage.Fit("Longinus",
("Cockpit 2x2", int2(2, 6), ItemRotation.None), ("Compact Reactor", int2(2, 4), ItemRotation.None),
@ -362,7 +362,7 @@ public sealed partial class RunStartTests
var forward = largeDrives.Sum(t => t.Thrust) / mass;
Assert.InRange(envelope.Forward, forward * .99f, forward * 1.01f);
Assert.Equal(0f, envelope.Reverse);
// One Talaria pushes each way, and each has its own lot's thrust: the sides are the two flanks' accelerations.
// One Rugged Drive pushes each way, and each has its own lot's thrust: the sides are the two flanks' accelerations.
var flankAccelerations = flanks.Select(t => t.Thrust / mass).OrderBy(a => a).ToArray();
var sides = new[] { envelope.Left, envelope.Right }.OrderBy(a => a).ToArray();
Assert.InRange(sides[0], flankAccelerations[0] * .99f, flankAccelerations[0] * 1.01f);
@ -580,7 +580,7 @@ public sealed partial class RunStartTests
public Weapon Gun, Laser, GunnerLaser;
}
// The target's fit, written out: the Djinni's own hardpoints filled with the Talaria it is generated with, its
// The target's fit, written out: the Djinni's own hardpoints filled with the Rugged Drive it is generated with, its
// reactor, cockpit and radiators. What the evasion tests price is this ship's strafing, not whatever the generator
// rolls for it from the galaxy.
private static ScenarioFit EvDjinni(ScenarioStage stage) => stage.Fit("Djinni",

View file

@ -454,15 +454,15 @@ public sealed partial class RunStartTests
Assert.True(clearPath <= .25f, $"the machine gun does not: {clearPath}");
}
// Data check, not a behaviour test: ruling plight-shape, plight is one wide, two high and fits a Longinus Energy hardpoint.
// Data check, not a behaviour test: ruling plight-shape, the Lightning Projector is one wide, two high and fits a Longinus Energy hardpoint.
[Fact]
public void PlightHasItsRuledShape()
{
var plight = _cache.GetAll<WeaponItemData>().Single(w => w.Name == "Lightning Projector");
Assert.Equal(1, plight.Shape.Width);
Assert.Equal(2, plight.Shape.Height);
Assert.True(plight.Shape.Cells[0, 0] && plight.Shape.Cells[0, 1], "both cells are occupied");
var projector = _cache.GetAll<WeaponItemData>().Single(w => w.Name == "Lightning Projector");
Assert.Equal(1, projector.Shape.Width);
Assert.Equal(2, projector.Shape.Height);
Assert.True(projector.Shape.Cells[0, 0] && projector.Shape.Cells[0, 1], "both cells are occupied");
var longinus = _cache.GetAll<HullData>().Single(h => h.Name == "Longinus");
Assert.Contains(longinus.Hardpoints, hardpoint => hardpoint.Type == HardpointType.Energy && hardpoint.Takes(plight));
Assert.Contains(longinus.Hardpoints, hardpoint => hardpoint.Type == HardpointType.Energy && hardpoint.Takes(projector));
}
}

View file

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GameCult.Caching;
using Xunit;
// AetherDb's generic-designs command (aetheria-release, cut generic-design-names), run on a scratch copy of the shipped
@ -97,6 +99,65 @@ public sealed class GenericDesignsCommandTests : IDisposable
});
}
// Re-spell every record's payload in a longer, equally valid MessagePack form (an array16 header for the fixarray one). The
// catalog then holds bytes no re-encode through the current types would reproduce, so a write that re-encodes records it did
// not author shows as changed bytes; a write that lands onto the file as it is leaves them alone.
private int WidenEveryPayload()
{
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(CatalogPath));
var widened = 0;
foreach (var record in snapshot.Records)
{
var payload = record.Payload;
if (payload.Length == 0 || (payload[0] & 0xF0) != 0x90) continue;
record.Payload = new byte[] { 0xDC, 0x00, (byte)(payload[0] & 0x0F) }.Concat(payload.Skip(1)).ToArray();
widened++;
}
File.WriteAllBytes(CatalogPath, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
return widened;
}
private Dictionary<string, CultPersistedRecord> Records() =>
CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(CatalogPath)).Records.ToDictionary(r => r.Key, StringComparer.Ordinal);
[Fact]
public void Apply_writes_only_the_records_it_authors_and_restores_the_rewritten_design_texts()
{
var (largeDriveText, tractorText, smokeText, tractorProductText) = Read(db => (DesignNamed(db, "Large Drive").Description, DesignNamed(db, "Tractor Beam").Description,
DesignNamed(db, "Smoke Proximity").Description, ProductNamed(db, "Space Tractor").Description));
Edit(db =>
{
var drive = DesignNamed(db, "Large Drive");
drive.Description = "Keeps you going and going and going...";
Save(db, drive);
var smoke = DesignNamed(db, "Smoke Proximity");
smoke.Description = "A Spectra that bursts by proximity, blast 8. For scenarios; no one builds it.";
Save(db, smoke);
var tractor = ProductNamed(db, "Space Tractor");
tractor.Description = "It's a tractor. In space.";
Save(db, tractor);
});
Assert.True(WidenEveryPayload() > 100, "the scratch catalog should hold many records in the widened spelling");
var before = Records();
Assert.Equal(0, Program.GenericDesigns(apply: true, root: _root));
var after = Records();
Assert.Equal(before.Count, after.Count);
var changed = before.Keys.Where(key => !before[key].Payload.AsSpan().SequenceEqual(after[key].Payload)).ToArray();
Assert.Equal(3, changed.Length);
foreach (var (key, record) in before.Where(pair => !changed.Contains(pair.Key)))
Assert.True(record.Payload.AsSpan().SequenceEqual(after[key].Payload), "a record the command did not author has changed bytes");
Read(db =>
{
Assert.Equal(largeDriveText, DesignNamed(db, "Large Drive").Description);
Assert.Equal(tractorText, DesignNamed(db, "Tractor Beam").Description);
Assert.Equal(smokeText, DesignNamed(db, "Smoke Proximity").Description);
Assert.Equal(tractorProductText, ProductNamed(db, "Space Tractor").Description);
return 0;
});
}
[Fact]
public void A_generic_name_held_by_another_design_is_refused_and_nothing_is_written()
{

View file

@ -247,7 +247,7 @@ public sealed class MunitionPartsTests : IDisposable
private static readonly (string Name, int X, int Y)[] ShipGear =
{ ("Large Drive", 1, 0), ("Large Drive", 3, 0), ("Compact Reactor", 2, 4), ("Cockpit 2x2", 2, 6), ("Compact Sensor", 3, 10) };
// A GT 3K fitted to a Longinus launcher hardpoint beside that gear, its lot's thruster role at the given quality.
// A Guided Missile Launcher fitted to a Longinus launcher hardpoint beside that gear, its lot's thruster role at the given quality.
private static EquippedItem Fit(CultCache cache, float thrusterQuality, out Ship ship)
{
var hull = cache.GetByName<HullData>("Longinus");

View file

@ -298,7 +298,7 @@ public sealed class RestoredHullsTests
Assert.True(ship.TryEquip(gearItem, hardpoint.Position));
}
// A reactor: Victoire (one of Cut 1's own restored thrusters, docs/locomotion-cut.md's own note on it)
// A reactor: Racing Drive (one of Cut 1's own restored thrusters, docs/locomotion-cut.md's own note on it)
// carries a real EnergyUsage draw unlike its four siblings, so an unpowered ship reads as though only
// some fitting thrusters produce thrust -- an artifact of which design TryEquip's own hardpoint search
// happens to land on, not of the restored content. Every hull here also has a Reactor hardpoint; a real

View file

@ -580,4 +580,19 @@ public sealed partial class RunStartTests
Assert.Equal("<Keyboard>/1", Assert.Single(settings.InputSettings.ActionBarInputs));
Assert.Equal((Quality.High, true), (settings.GraphicsSettings.NebulaQuality, settings.GraphicsSettings.ShowAsteroidsInMinimap));
}
// A scenario's Brief is player text (the menu shows it): it names gear by what the catalog sells today. A name a rename retired,
// that no design or product carries any more, would send the player looking for gear that is gone.
[Fact]
public void NoScenarioBriefNamesRetiredGear()
{
var current = _cache.GetAll<EquippableItemData>().Select(design => design.Name)
.Concat(_cache.GetAll<FactionProductData>().Select(product => product.Name)).ToHashSet(StringComparer.Ordinal);
var stale = new List<string>();
foreach (var scenario in EveryScenario)
foreach (var retired in Program.Renames.Select(row => row.Old).Where(name => !current.Contains(name)))
if (System.Text.RegularExpressions.Regex.IsMatch(scenario.Brief, @"(?<!\w)" + System.Text.RegularExpressions.Regex.Escape(retired) + @"s?(?!\w)"))
stale.Add(scenario.Name + " names " + retired);
Assert.True(stale.Count == 0, "scenario briefs naming gear the catalog no longer has: " + string.Join("; ", stale));
}
}

View file

@ -12,7 +12,7 @@ public sealed partial class RunStartTests
private static List<EquippedItem> WeaponMounts(Entity ship) =>
ship.Equipment.Where(item => item.Behaviors.OfType<Weapon>().Any()).ToList();
// Weapon Feel: the player flies the Smoke Test Hull with ChargeBlast SG on the 2x2 Energy hardpoint, Autocannon
// Weapon Feel: the player flies the Smoke Test Hull with Charged Scatter Blaster on the 2x2 Energy hardpoint, Autocannon
// on the 2x2 Ballistic hardpoint and a burst weapon; the bare hull ahead lies in every mount's arc.
[Fact]
public void WeaponFeelConditions()

View file

@ -43,7 +43,7 @@ public sealed class SmokeRoundsCommandTests : IDisposable
public void Dispose() => Directory.Delete(_root, true);
private (WeaponItemData[] Smoke, WeaponItemData Spectra, WeaponItemData ChargeBlast, int Products) Read()
private (WeaponItemData[] Smoke, WeaponItemData LightLaser, WeaponItemData ChargedScatterBlaster, int Products) Read()
{
var db = AetherDb.Open(root: _root);
try
@ -85,7 +85,7 @@ public sealed class SmokeRoundsCommandTests : IDisposable
Assert.Equal((WeaponFuse.Delayed, 4f, 2f, 2f), (Design("Smoke Delayed").Fuse.Value, Design("Smoke Delayed").BlastRadius.Value, Data("Smoke Delayed").Penetration.Min, Data("Smoke Delayed").Penetration.Max));
Assert.Equal((WeaponFuse.Proximity, 30f, 20f, 20f), (Design("Smoke Refused").Fuse.Value, Design("Smoke Refused").BlastRadius.Value, Data("Smoke Refused").Range.Min, Data("Smoke Refused").Range.Max));
// Spectra clones are energy, 1x2, with the Spectra's own gear; the charged one is a ChargeBlast SG clone on its 2x2 energy mount.
// Light Laser clones are energy, 1x2, with the Light Laser's own gear; the charged one is a Charged Scatter Blaster clone on its 2x2 energy mount.
foreach (var name in new[] { "Smoke Proximity", "Smoke Delayed", "Smoke Refused", "Smoke Refused Beam" })
Assert.Equal((spectra.Hardpoint, spectra.Shape.Width, spectra.Shape.Height, spectra.Mass), (Design(name).Hardpoint, Design(name).Shape.Width, Design(name).Shape.Height, Design(name).Mass));
Assert.Equal((chargeBlast.Hardpoint, chargeBlast.Shape.Width, chargeBlast.Shape.Height), (Design("Smoke Refused Charge").Hardpoint, 2, 2));

View file

@ -20,7 +20,7 @@ public sealed partial class RunStartTests
return (arena, staged);
}
// Fused Rounds, nothing selected, the player aiming down the bare hull ahead. The DeathCluster is the control: it fires
// Fused Rounds, nothing selected, the player aiming down the bare hull ahead. The Cluster Cannon is the control: it fires
// in this arena, so a fused weapon that does not is not a harness that cannot fire. A fused round flies slowly enough
// to be in the air when the direct weapon kills the fragile hull (check A.3) and bursts on the first hull on the aim line.
[Theory]
@ -51,7 +51,7 @@ public sealed partial class RunStartTests
Assert.InRange(at + burst.ArrivalIn - fired[0], 1f, 5f); // in the air for seconds: not resolved in its firing tick, not minutes of flight
}
// Check A.3 flown: the fragile hull is selected, a Smoke Proximity round is fired at it and the DeathCluster kills it
// Check A.3 flown: the fragile hull is selected, a Smoke Proximity round is fired at it and the Cluster Cannon kills it
// before the round lands, so the round never bursts.
[Fact]
public void TheDirectWeaponKillsTheFragileHullWhileASmokeRoundIsStillInTheAir()

View file

@ -14,7 +14,7 @@ using float3 = CultMath.float3;
// Flight control allocates thrust (ruling flight-control-allocates-thrust): Ship.Update hands the intent and every
// thruster's live column to one ThrustAllocator, and a ship flies on what that answers. These tests fly the Duel's
// Longinus (two mismatched Large Drives forward, a Talaria on each flank) through the real Ship.Update; a test that reads
// Longinus (two mismatched Large Drives forward, a Rugged Drive on each flank) through the real Ship.Update; a test that reads
// velocity steps the arena, because the zone places the orbits that Ship.Update alone leaves stacked at the spawn.
public sealed partial class RunStartTests
{
@ -307,7 +307,7 @@ public sealed partial class RunStartTests
// Rulings evasion-is-unpredictability and turn-authority-full: the Longinus's Large Drives count toward its turn,
// because the envelope is summed over every live column. Its clockwise is the sum of the positive yaw of its columns
// at live thrust, and exceeds the Talaria's own by the drives'.
// at live thrust, and exceeds the Rugged Drive's own by the drives'.
[Fact]
public void TheEnvelopeCountsTheMains()
{

View file

@ -18,13 +18,13 @@ public class ThrustAllocatorTests
static float3 Col(float starboard, float forward, float yawDeg) => new(starboard, forward, yawDeg * Deg);
// TA3, the Duel Longinus: two mismatched drives (0, 1) and two Talarias (2 clockwise, 3 counter-clockwise).
// TA3, the Duel Longinus: two mismatched drives (0, 1) and two Rugged Drives (2 clockwise, 3 counter-clockwise).
static float3[] Duel() => new[]
{
Col(0, 53.94f, 49.24f), Col(0, 61.19f, -55.86f), Col(29.08f, 0, 166.26f), Col(-21.80f, 0, -124.63f),
};
// The clockwise Talaria lost.
// The clockwise Rugged Drive lost.
static float3[] DamagedDuel() => new[]
{
Col(0, 53.94f, 49.24f), Col(0, 61.19f, -55.86f), default, Col(-21.80f, 0, -124.63f),
@ -137,7 +137,7 @@ public class ThrustAllocatorTests
[Fact]
public void TheOnsetMovesWithTheColumns()
{
// Clockwise Talaria lost: the drive serves a clockwise turn from the first touch.
// Clockwise Rugged Drive lost: the drive serves a clockwise turn from the first touch.
var damaged = DamagedDuel();
var small = Solve(damaged, 0, 0, .01f);
Assert.True(small[0] > 0, $"drive {small[0]}");
@ -145,7 +145,7 @@ public class ThrustAllocatorTests
var full = Solve(damaged, 0, 0, 1f);
Assert.True(MathF.Abs(YawDeg(damaged, full) - 49.24f) <= 49.24f * 1e-3f, $"yaw {YawDeg(damaged, full)}");
// Clockwise Talaria halved: the onset is its new share of the clockwise extreme.
// Clockwise Rugged Drive halved: the onset is its new share of the clockwise extreme.
var halved = Duel();
halved[2] = halved[2] * .5f;
var onset = halved[2].z / Extremes(halved, 2).plus;

View file

@ -607,10 +607,14 @@ public static class Program
// Validate comment) -- so it calls the same validation directly, right before the identity-preserving
// write, instead of writing raw and unvalidated the way this line used to.
foreach (var (document, _) in changed) CultRecordRefs.Validate(document);
db.Cache.Commit(batch =>
// 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($"Landed {changed.Count} changed records in Aetheria.cc");
return 0;
}
@ -1714,10 +1718,10 @@ public static class Program
}
static PerformanceStat Fixed(float value) => new PerformanceStat { Min = value, Max = value, Terms = new List<StatTerm>() };
var proximity = Copy("Light Laser", names[0], "A Spectra that bursts by proximity, blast 8. For scenarios; no one builds it.", WeaponFuse.Proximity, 8f);
var delayed = Copy("Light Laser", 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("Light Laser", 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("Charged Scatter Blaster", 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 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,
@ -2361,6 +2365,20 @@ public static class Program
"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
@ -2417,6 +2435,15 @@ public static class Program
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();