Move every LonginusX consumer to the Longinus: scenarios, tests, tools, the starting hull

Duel, Arcs, Launcher Angles and Starved Reactor fit and place the Longinus
(thrusters, Core Power or MoveOnPro at (2,4), radiators relaid); the modes start
a Longinus (Settings.asset StartingHullName; the Aether multipliers leave it).
The drive's tests, its Steering Axis probe and its Brownout fixtures go with their
subject; ship_cc.py and the live-game checklist drop the drive and the hull.
The drive itself and the catalog records are deleted in the following commits.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-08 13:44:48 +02:00
commit bf17314765
17 changed files with 79 additions and 145 deletions

BIN
Assets/Resources/Settings.asset (Stored with Git LFS)

Binary file not shown.

View file

@ -16,18 +16,21 @@ public sealed class Arcs : Scenario
public override void Stage(ScenarioStage stage)
{
var fighter = stage.Fit("LonginusX",
var fighter = stage.Fit("Longinus",
("Cockpit 2x2", int2(2, 6), None),
("Traction", int2(2, 3), None),
("Core Power", int2(2, 1), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("Core Power", int2(2, 4), None),
("GT 3K", int2(0, 5), None),
("GT 3K", int2(5, 5), None),
("FastBlast+-", int2(1, 8), None),
("FastBlast+-", int2(4, 8), None),
("Iapyx", int2(1, 2), CounterClockwise),
("Iapyx", int2(4, 2), Clockwise),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("Fire Control Array", int2(2, 5), None));
("Fire Control Array", int2(1, 6), None));
var turret = stage.Fit("Turret",
("ClearPath", int2(1, 2), None),
("ClearPath", int2(5, 2), None),
@ -39,8 +42,8 @@ public sealed class Arcs : Scenario
("Fire Control Array", int2(3, 6), None));
stage.Player(fighter, float2(0, 0), facing: float2(0, 1));
stage.Place(stage.Bare("LonginusX"), Bow);
stage.Place(stage.Bare("LonginusX"), Stern);
stage.Place(stage.Bare("Longinus"), Bow);
stage.Place(stage.Bare("Longinus"), Stern);
stage.Place(turret, Turret, stance: ScenarioStance.Hostile);
}
}

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 LonginusX with slow GT 3K missiles and a Spectra in its hold. Verify: rolled " +
public override string Brief => "One piloted, hostile Longinus with slow GT 3K missiles and a Spectra 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;
@ -13,19 +13,22 @@ public sealed class Duel : Scenario
public override void Stage(ScenarioStage stage)
{
var fighter = stage.Fit("LonginusX",
var fighter = stage.Fit("Longinus",
("Cockpit 2x2", int2(2, 6), None),
("Traction", int2(2, 3), None),
("Core Power", int2(2, 1), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("Core Power", int2(2, 4), None),
("GT 3K", int2(0, 5), None),
("GT 3K", int2(5, 5), None),
("FastBlast+-", int2(1, 8), None),
("FastBlast+-", int2(4, 8), None),
("Iapyx", int2(1, 2), CounterClockwise),
("Iapyx", int2(4, 2), Clockwise),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("PotaT+-", int2(1, 5), None),
("Fire Control Array", int2(2, 5), None),
("Fire Control Array", int2(1, 6), None),
("Store-All Plus", int2(2, 8), None));
stage.Player(fighter, float2(0, 0));

View file

@ -4,7 +4,7 @@ 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 LonginusX broadside on, a bare Longinus beside it. " +
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 uint Seed => 105;
public override bool Ambient => false;
@ -13,21 +13,24 @@ public sealed class LauncherAngles : Scenario
public override void Stage(ScenarioStage stage)
{
var launcher = stage.Fit("LonginusX",
var launcher = stage.Fit("Longinus",
("Cockpit 2x2", int2(2, 6), None),
("Traction", int2(2, 3), None),
("Core Power", int2(2, 1), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("Core Power", int2(2, 4), None),
("GT 3K", int2(0, 5), None),
("pswarm", int2(5, 5), None),
("FastBlast+-", int2(1, 8), None),
("FastBlast+-", int2(4, 8), None),
("Iapyx", int2(1, 2), CounterClockwise),
("Iapyx", int2(4, 2), Clockwise),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("Fire Control Array", int2(2, 5), None));
("Fire Control Array", int2(1, 6), None));
stage.Player(launcher, float2(0, 0));
stage.Place(stage.Bare("LonginusX"), float2(0, 900), facing: float2(1, 0), stance: ScenarioStance.Hostile);
stage.Place(stage.Bare("Longinus"), float2(0, 900), facing: float2(1, 0), stance: ScenarioStance.Hostile);
stage.Place(stage.Bare("Longinus"), float2(300, 700));
}
}

View file

@ -13,17 +13,20 @@ public sealed class StarvedReactor : Scenario
public override void Stage(ScenarioStage stage)
{
var starved = stage.Fit("LonginusX",
var starved = stage.Fit("Longinus",
("Cockpit 2x2", int2(2, 6), None),
("Traction", int2(2, 3), None),
("MoveOnPro", int2(2, 1), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("MoveOnPro", int2(2, 4), None),
("Spectra", int2(1, 8), None),
("Spectra", int2(4, 8), None),
("Iapyx", int2(1, 2), CounterClockwise),
("Iapyx", int2(4, 2), Clockwise),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("Fire Control Array", int2(2, 5), None));
("Fire Control Array", int2(1, 6), None));
stage.Player(starved, float2(0, 0));
stage.Place(stage.Bare("LonginusX"), float2(0, 400));
stage.Place(stage.Bare("Longinus"), float2(0, 400));
}
}

View file

@ -33,7 +33,7 @@ Compiled 2026-09-29 by Eyes from the maps on the branch. Pointers are `doc:line`
### 2b. Fresh launch (no `run.cc`, no `player.cc`)
3. **Launch to main menu.** Expect no Addressables errors, and the Groups window shows `Assets/Content` under `Default Local Group` (`addressables-cut.md:301`). `player.cc` appears and Continue is disabled (`cultcache-migration-cut.md:2746`). Proves: Addressables Cut 1, CultCache cutover. The 09-17 smoke covered the last two on the old tree; this is a cheap recheck on the new schema.
4. **New Game (tutorial).** Expect the galaxy generates and the start ship is LonginusX (`settings-globals-cut.md:498`, an expectation, not a recorded check for this stack). Expect a Zenith station with a docking bay in the entrance zone every time (`locomotion-cut.md:375`). Proves: locomotion Cut 1 station rule.
4. **New Game (tutorial).** Expect the galaxy generates and the start ship is the Longinus (`settings-globals-cut.md:498`, an expectation, not a recorded check for this stack). Expect a Zenith station with a docking bay in the entrance zone every time (`locomotion-cut.md:375`). Proves: locomotion Cut 1 station rule.
5. **Addressables, "Use Asset Database".** Ships, turret and Zenith spawn; thruster and aether-drive particles show; fire an instant weapon (Autocannon) and a charged one (ChargeBlast SG); action bar icons show (`addressables-cut.md:362-366`). Proves: Addressables Cut 2.
### 2c. Flight, power and economy
@ -44,19 +44,19 @@ Compiled 2026-09-29 by Eyes from the maps on the branch. Pointers are `doc:line`
9. **Brownout and refill (O6, O4).** Brownout reads as degradation, not breakage (`:774`, `:990`). A weapon refilling under brownout stutters instead of firing full rate and cooking the ship (`:729-730`). Proves: power-supply term, brownout, input capacitors.
10. **Memory across zones (O1).** Play across several zones with kills, open the Profiler or Task Manager, and confirm memory does not climb (`:672-673`, `:980-982`). Proves: stat resolver, the entity-leak fix (map Cut 2). Combine with steps 11 and 12.
### 2d. Combat (against a LonginusX-class AI), then save and Studio
### 2d. Combat (against a Longinus-class AI), then save and Studio
11. **Fire-control Cut 1 arcs.** Side-mounted weapons still fire at targets abeam; a turret tracks all the way round; nothing fires through the hull (`fire-control-cut.md:397-398`).
12. **Fire-control Cut 3 and shots.** Shots show rolled impacts and misses; a miss reads as a near-miss, not a bug; damage matches the HUD; aiming at a revealed subsystem concentrates damage; kill time is in the same order as before (`:659-661`). Reveal and selection work, kills drop loot, pickup stores it, the tractor beam still pulls (`headless-playground-cut.md:789-794`). No crash or frozen hot ship when the target vanishes (the 2026-09-20 failure, `fire-control-cut.md:1464-1470`). Recheck after the fixes: unrecorded.
13. **12.2, bow then beam.** The HUD `hull` factor reads higher from the beam. Turn your armoured face into a slow missile and watch where it lands (`:2380-2383`).
14. **12.3, launcher into a bow, then a flank.** Use a GT 3K or pswarm launcher. The schematic display pulses the facing edge only (`:2519-2520`).
15. **12.4, scratch catalog.** Watch a proximity airburst beside a LonginusX and a delayed penetrator into its nose. Damage must land where the model shows the blast; this confirms the centre-of-mass anchor. Bow is already confirmed +y (`:3095-3097`, `:2028-2031`). Proves: 12.4(b) `Detonate`.
15. **12.4, scratch catalog.** Watch a proximity airburst beside a Longinus and a delayed penetrator into its nose. Damage must land where the model shows the blast; this confirms the centre-of-mass anchor. Bow is already confirmed +y (`:3095-3097`, `:2028-2031`). Proves: 12.4(b) `Detonate`.
16. **Wormhole, save, Continue.** Take a wormhole, quit, Continue: same items, same tier and brand (`item-provenance-cut.md:536-537`).
17. **Die.** Continue is disabled and `run.cc` holds no ledger (`:540-541`).
18. **Shield panel, `FieldShieldTest`.** Follow the numbered look in `shield-panel-cut.md:683-700`. Nothing visible on load. Click the nose: a patch grows and the field ripple runs too. Click the flank: the panel lies flat, not tilted. Click the same spot twice: it strikes the same panel. Spam far-apart clicks: at most 12 panels, oldest replaced. Let them fade: none left. Then the Cut 5 payoff: strong hits dice the patch, and repeated hits on one spot break through on a later hit (`:737-739`). Proves: shield-panel Cuts 3, 4, 5.
19. **Studio click-through.**
- Open `Aetheria.cc`. The fire-control behaviour union renders (`fire-control-cut.md:503-506`).
- `LonginusX` schematic still underlays the shape drawer (`addressables-cut.md:370-371`).
- `Longinus` schematic still underlays the shape drawer (`addressables-cut.md:370-371`).
- The restored hull records open (`locomotion-cut.md:501`; the map gives no checklist beyond "the Studio click-through").
- The scratch catalog's new `Fuse` and `BlastRadius` fields edit (from 2a; no map line).
- Open `GameData/run.cc` after step 16. `aetheria.provenanceledger` renders `Lots` with `Attributed` origins and per-role lists. If Studio cannot draw an int-keyed dictionary of unions, that is a Studio defect to raise, not a schema change (`item-provenance-cut.md:541-544`).

View file

@ -10,7 +10,7 @@ using static CultMath.math;
// Cut 7 (docs/stats-and-power-target.md): "Continuous consumers brown out through a power supply curve on
// their performance stats. An exponent is enough." Cut 6 wired the curve itself (PowerCurveTests.cs) and the
// bus already writes a real fractional grant (PowerBusTests.cs, PowerTierTests.cs), but every continuous
// consumer still gated its own effect on Item.PowerSupply >= 1f (Thruster.Execute, AetherDrive.Execute,
// consumer still gated its own effect on Item.PowerSupply >= 1f (Thruster.Execute,
// Radiator.Execute, ConstantWeapon.Execute, EnergyDraw.Execute) -- so a partial grant read exactly like none,
// the flicking-off bug the ruling names. This file pins each behaviour's own gate removal: a partial grant now
// produces a reduced effect, a full grant is unchanged, a zero grant still produces nothing, the degradation
@ -18,12 +18,11 @@ using static CultMath.math;
// rule, Cut 4) is untouched by any of it. Each rule has a matching mutation in
// tests/mutation_tests_stats_power_cut7.py.
//
// F6 (docs/stats-and-power-cut.md, Soul pass 2026-09-19) surfaced a real conflict this file's Radiator and
// AetherDrive fixtures happened to sit on top of, unnoticed because the pre-F6 registry only named each
// F6 (docs/stats-and-power-cut.md, Soul pass 2026-09-19) surfaced a real conflict this file's Radiator
// fixture happened to sit on top of, unnoticed because the pre-F6 registry only named each
// consumer's own top-level request field. Radiator.PowerRequest's own early-out reads PumpedHeat AND WasteHeat
// (to decide whether the pump can keep up this tick); AetherDrive.PowerRequest's spin-up arithmetic reads
// Torque, LambdaMultiplier, MaximumRpm and PassiveCoupling. Cut 7 asks for the brownout curve to live on exactly
// the stats these two behaviours actually curve (PumpedHeat, Torque), but F6 forbade a PowerSupply term on any
// (to decide whether the pump can keep up this tick). Cut 7 asks for the brownout curve to live on exactly
// the stats the behaviour actually curves (PumpedHeat), but F6 forbade a PowerSupply term on any
// stat a request reads at all, on pain of the exact oscillation Soul measured against the shipped catalog's own
// "OK Disperser" radiator (SOUL_RadiatorRequestDependsOnItsOwnGrantAndOscillates, docs/stats-and-power-cut.md).
// The six tests below that authored one were marked Skip rather than deleted or silently reworked to hide the
@ -32,8 +31,8 @@ using static CultMath.math;
//
// Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19) is that future pass: a power
// request is evaluated nominally -- what the item wants at full supply, not what it is currently managing
// (EquippedItem.EvaluateNominalPower, Entity.cs). Radiator.PowerRequest and AetherDrive.PowerRequest now read
// PumpedHeat/Torque (and every other field in StatValidation.PowerRequestFields) through that nominal read, so
// (EquippedItem.EvaluateNominalPower, Entity.cs). Radiator.PowerRequest now reads
// PumpedHeat (and every other field in StatValidation.PowerRequestFields) through that nominal read, so
// the same stat can carry a PowerSupply term (curved for Execute's real effect) AND be a request field, without
// the request depending on its own answer -- there is no more oscillation to reproduce, because the request no
// longer reads the live grant at all. StatValidation.ValidateNoPowerSupplyOnRequest (the static check F6 added
@ -64,14 +63,6 @@ public sealed class BrownoutTests : IDisposable
private static PerformanceStat Curved(float max, float exponent) =>
new PerformanceStat { Min = 0, Max = max, Terms = { new StatTerm { Source = StatSource.PowerSupply, Exponent = exponent } } };
// A flat Bezier (two keys, equal value, zero tangents): AetherDrive's TorqueProfile multiplies potential
// torque by this curve's value at the current rpm ratio -- flat at 1 keeps that multiplier out of the way of
// the PowerSupply curve under test.
private static BezierCurve FlatCurve(float value) => new BezierCurve
{
Keys = new[] { new float4(0, value, 0, 0), new float4(1, value, 0, 0) }
};
private static EquippableItem Mint(CultCache cache, ItemManager items, ItemData design) => new EquippableItem
{
Data = cache.RefOf<ItemData>(design),
@ -180,55 +171,6 @@ public sealed class BrownoutTests : IDisposable
Assert.Equal(.025f, half, 3);
}
// --- AetherDrive: PassiveCoupling = 1 engages the rotor regardless of axis input, so the drive can be
// --- exercised without threading Ship's own movement/torque classification -- Axis stays at Ship.Update's
// --- own default (0,0,Turn), and Turn is 0 by default. Rpm starts at 0,
// --- so decay(0, ...) contributes nothing and the whole tick's Rpm gain is exactly this behaviour's own
// --- spin-up under test. ---
private (float rpmGain, AetherDrive drive) RunAetherDrive(float reactorCharge, float exponent = 1)
{
using var cache = OpenCatalog(new AetherDriveData
{
RotorDiameter = float3(1, 1, 1), RotorMass = float3(1, 1, 1),
MaximumRpm = Constant(100), CouplingLambda = float3(1, 1, 1), LambdaMultiplier = Constant(1),
CouplingEfficiency = Constant(1), Torque = Curved(30, exponent), TorqueProfile = FlatCurve(1),
EnergyDraw = Constant(50), PassiveCoupling = Constant(1)
});
var ship = BuildShip(cache, reactorCharge);
ship.Update(1f);
var drive = ship.GetBehavior<AetherDrive>();
return (drive.Rpm.x, drive);
}
// Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19, see file header): Torque
// is a registered AetherDrive request field (PowerRequest's spin-up arithmetic reads it), but PowerRequest now
// reads it through EvaluateNominalPower (full-supply, undegraded), so Curved(...) on Torque is a legal
// catalog shape again -- Execute's own real, curved Evaluate is what actually degrades the spin-up below.
[Fact]
public void AetherDriveAtHalfGrantProducesReducedNotZeroNotFullSpinUp()
{
var (full, _) = RunAetherDrive(reactorCharge: 1000); // demand ~50, generation far exceeds it -> ratio 1
var (half, _) = RunAetherDrive(reactorCharge: 25); // demand ~50, generation 25 -> ratio .5
Assert.True(half > 0f && half < full, $"half={half} should sit strictly between 0 and full={full}");
}
[Fact]
public void AetherDriveAtFullGrantIsUnchangedFromToday()
{
var (full, drive) = RunAetherDrive(reactorCharge: 1000);
Assert.Equal(1f, drive.Item.PowerSupply, 3);
Assert.True(full > 0f);
}
[Fact]
public void AetherDriveAtZeroGrantProducesNothing()
{
var (zero, drive) = RunAetherDrive(reactorCharge: 0);
Assert.Equal(0f, drive.Item.PowerSupply, 3);
Assert.Equal(0f, zero, 4);
}
// --- Radiator: no explicit power gate survives Cut 7 at all -- PumpedHeat above is a plain Evaluate() read,
// --- so a curved PumpedHeat already pumps less under a partial grant, and waste heat is unaffected, so a
// --- starved radiator falls behind (the reduced-performance failure the ruling asks for) instead of the
@ -383,10 +325,10 @@ public sealed class BrownoutTests : IDisposable
// --- Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19): a direct probe of
// --- EquippedItem.EvaluateNominalPower itself, decoupled from any behaviour's own physical state (Radiator's
// --- temperature, AetherDrive's Rpm) so the only thing that can make it diverge from the ordinary Evaluate
// --- temperature) so the only thing that can make it diverge from the ordinary Evaluate
// --- is the item's own PowerSupply grant. Thruster's EnergyUsage carries the term here purely as a probe --
// --- not a shape BrownoutTests' other fixtures author -- because Thruster has no other tick-to-tick state
// --- that could confound the comparison the way Radiator's RadiatorTemperature or AetherDrive's Rpm would.
// --- that could confound the comparison the way Radiator's RadiatorTemperature would.
// --- Backs tests/mutation_tests_stats_power_nominal_request.py's own mutation of this exact method. ---
[Fact]
public void EvaluateNominalPowerIgnoresTheItemsCurrentPowerSupplyGrant()
@ -444,16 +386,4 @@ public sealed class BrownoutTests : IDisposable
$"expected tick 2's gate to stay open on the nominal PumpedHeat/WasteHeat ratio, got demand {secondDemand}");
}
// --- The same wiring check attempted on AetherDrive.PowerRequest's own Torque read does not carry over
// --- cleanly: torqueRatio (actualRpmDelta/potentialRpmDelta) self-cancels Torque's own magnitude whenever
// --- the rotor is not yet rpm-capped (both numerator and denominator scale with Torque, so the ratio -- and
// --- so the final draw -- comes out identical whether Torque is read nominally or through the curved
// --- Evaluate; verified empirically while building this fixture: reverting AetherDrive.PowerRequest to
// --- Evaluate produced byte-identical TotalDemand across two ticks, including once the rotor was pushed
// --- into its rpm cap). A black-box demand comparison cannot discriminate this call site the way it can
// --- Radiator's binary gate above. Its correctness rests instead on the identical pattern to Radiator's
// --- (same EvaluateNominalPower call, same EquippedItem mechanism EvaluateNominalPowerIgnoresTheItemsCurrent
// --- PowerSupplyGrant already pins) and on AetherDriveAtHalfGrantProducesReducedNotZeroNotFullSpinUp/
// --- AtFullGrantIsUnchangedFromToday/AtZeroGrantProducesNothing above, which exercise the real end-to-end
// --- Execute path this ruling exists to keep legal. Not part of the mutation harness for that reason.
}

View file

@ -12,7 +12,7 @@ using static CultMath.math;
// Should multiply that by actual performance." EquippedItem.ConditionRatio (Entity.cs) is the one number that
// answers "how healthy does this item actually look": the item's resolved value for a stat against what the
// same item, same lot, same modifiers would produce with Heat/Durability/PowerSupply pinned to their identity.
// Thruster.Condition and AetherDrive.Condition (Thruster.cs/AetherDrive.cs) are that ratio read against each
// Thruster.Condition (Thruster.cs) is that ratio read against each
// behaviour's own governing stat (Thrust, Torque); ShipInstance.cs (Unity-side, not covered here) multiplies
// presentation intent by it instead of recomputing any durability/heat/power arithmetic of its own.
// Each rule here has a matching mutation in tests/mutation_tests_condition_ratio_cut8.py.

View file

@ -469,10 +469,9 @@ public sealed class FireControlCut124Tests : IDisposable
}
// The same rule on the shipped hulls: a blast just grazing the corner farthest from the centre of mass. The
// corners of the shipped Longinus and LonginusX reach past the old cull's box-half-diagonal bound.
// corner of the shipped Longinus reaches past the old cull's box-half-diagonal bound.
[Theory]
[InlineData("Longinus")]
[InlineData("LonginusX")]
public void AGrazingBlastAtAShippedHullsFarthestCornerLands(string hullName)
{
Shape shape;

View file

@ -212,7 +212,7 @@ public sealed class FireControlCut7Tests : IDisposable
Assert.NotEmpty(equippable);
Assert.Contains(equippable, e => e is WeaponItemData);
var hull = cache.GetByName<HullData>("LonginusX");
var hull = cache.GetByName<HullData>("Longinus");
Assert.NotNull(hull);
var settings = new GameplaySettings

View file

@ -670,7 +670,7 @@ public sealed partial class RunStartTests : IDisposable
public void TheRealHullsMaterializeBareFromABrandedProduct()
{
var available = new LoadoutGenerator(ref _items.Random, _items, _galaxy, _galaxy.Entrance, null, 2);
foreach (var hull in new[] { Hull("Djinni"), Hull("LonginusX"), TurretHull() })
foreach (var hull in new[] { Hull("Djinni"), Hull("Longinus"), TurretHull() })
{
var failures = new List<string>();
var entity = Loadouts.Materialize(_items, null, BareLoadout(hull), available.IsAvailable, failures);

View file

@ -129,7 +129,7 @@ public sealed partial class RunStartTests
[Fact]
public void ModesMatchOldNewGame()
{
Assert.Equal("LonginusX", _startingHull);
Assert.Equal("Longinus", _startingHull);
var tutorial = Launch(new TutorialGalaxy(), Inputs(() => GalaxySeed));
Assert.Empty(tutorial.failures);
@ -285,8 +285,8 @@ public sealed partial class RunStartTests
var failures = Refused(Arena(true), new Scripted(true, stage =>
{
stage.Player(stage.Bare("Djinni"), float2(0, 0));
stage.Place(stage.Generated("LonginusX"), float2(300, 0), stance: ScenarioStance.Hostile, piloted: true);
stage.Place(stage.Fit("LonginusX", ("No Such Gun", int2(1, 8), ItemRotation.None)), float2(-300, 0));
stage.Place(stage.Generated("Longinus"), float2(300, 0), stance: ScenarioStance.Hostile, piloted: true);
stage.Place(stage.Fit("Longinus", ("No Such Gun", int2(1, 8), ItemRotation.None)), float2(-300, 0));
}));
Assert.Contains("No Such Gun", Assert.Single(failures));
}
@ -301,8 +301,8 @@ public sealed partial class RunStartTests
var staged = Stage(zone, new Scripted(true, stage =>
{
stage.Player(stage.Bare("Djinni"), float2(0, 0));
stage.Place(stage.Generated("LonginusX"), float2(300, 0), stance: ScenarioStance.Hostile, piloted: true);
stage.Place(stage.Bare("LonginusX"), float2(-300, 0), stance: ScenarioStance.Neutral, piloted: false);
stage.Place(stage.Generated("Longinus"), float2(300, 0), stance: ScenarioStance.Hostile, piloted: true);
stage.Place(stage.Bare("Longinus"), float2(-300, 0), stance: ScenarioStance.Neutral, piloted: false);
}));
var player = staged.Player;
var hostile = staged.Entities[0];
@ -336,8 +336,8 @@ public sealed partial class RunStartTests
var staged = Stage(zone, new Scripted(true, stage =>
{
stage.Player(stage.Bare("Djinni"), float2(0, 0));
stage.Place(stage.Bare("LonginusX"), float2(300, 0), stance: ScenarioStance.Hostile);
stage.Place(stage.Bare("LonginusX"), float2(-300, 0), stance: ScenarioStance.Hostile);
stage.Place(stage.Bare("Longinus"), float2(300, 0), stance: ScenarioStance.Hostile);
stage.Place(stage.Bare("Longinus"), float2(-300, 0), stance: ScenarioStance.Hostile);
}));
var player = staged.Player;
var hostile = staged.Entities[0];

View file

@ -251,7 +251,7 @@ public sealed class ShipAuthoringTests
[Theory]
[InlineData(HardpointType.Hull), InlineData(HardpointType.Tool), InlineData(HardpointType.Thermal), InlineData(HardpointType.WarpDrive),
InlineData(HardpointType.Reactor), InlineData(HardpointType.Shield), InlineData(HardpointType.Sensors),
InlineData(HardpointType.ControlModule), InlineData(HardpointType.AetherDrive)]
InlineData(HardpointType.ControlModule)]
public void AnInternalHardpointCarriesNoAnchor(HardpointType type)
{
WithOnly(type).Validate();

View file

@ -33,7 +33,7 @@ public sealed class ShipValidationTests
var ship = ShipAuthoringTests.Fixture();
ship.Hull.Hardpoints[0].Armor = 5;
ship.Hull.Hardpoints[0].FiringArc = 360;
ship.Hull.Hardpoints[0].Type = HardpointType.AetherDrive;
ship.Hull.Hardpoints[0].Type = HardpointType.ControlModule;
ship.Visual.Anchors.RemoveAll(anchor => anchor.Id == "thruster.port");
ship.Hull.Hardpoints[0].Rotation = ItemRotation.Clockwise;
ship.Validate();

View file

@ -241,7 +241,7 @@ public sealed class SteeringTests
}
// The turn bound has one owner, the actuators: Ship.Update hands Turn to them unclamped, Thruster.Axis saturates to
// 0..1 and AetherDrive.Axis clamps to -1..1. A Turn of 3 therefore rotates the hull as a Turn of 1.
// 0..1. A Turn of 3 therefore rotates the hull as a Turn of 1.
[Fact]
public void TheActuatorsBoundATurnBeyondOne()
{
@ -276,13 +276,6 @@ public sealed class SteeringTests
thruster.Axis = -3f;
Assert.Equal(0f, thruster.Axis);
}
// Axis is a pure property; no catalog hull carries a drive, so the instance is made without its item.
var drive = (AetherDrive)System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof(AetherDrive));
drive.Axis = float3(3f, -3f, 3f);
Assert.Equal(float3(1f, -1f, 1f), drive.Axis);
drive.Axis = float3(0f, 0f, -3f);
Assert.Equal(-1f, drive.Axis.z);
}
// The size of a turn, computed from the hull and not read back from the code under test: each rotation thruster

View file

@ -33,7 +33,7 @@ UNITY_EXE = r"C:\Program Files\Unity\Hub\Editor\6000.3.24f1\Editor\Unity.exe"
ITEM_DATA_CS = REPO_ROOT / "Assets" / "Scripts" / "ServerShared" / "ItemData.cs"
DIRECTORY_BUILD_PROPS = REPO_ROOT / "Directory.Build.props"
# A known-good record to mutate: HullData "LonginusX", whose Prefab already resolves (see the Cut 2
# A known-good record to mutate: HullData "Longinus", whose Prefab already resolves (see the Cut 2
# migration report). Texture2D guid is its own Schematic (schema_Longinus.png), still a real,
# addressable asset, just the wrong type for Prefab (which wants a GameObject+EntityInstance).
TEXTURE_GUID = "51702555e534a7a4fb39eb105b80bbaf"
@ -109,7 +109,7 @@ using System.Linq;
using System.Reflection;
using GameCult.Caching;
// Scratch-only (not landed): sets HullData "LonginusX".Prefab to an arbitrary string in a copy of
// Scratch-only (not landed): sets HullData "Longinus".Prefab to an arbitrary string in a copy of
// the catalog, for tests/mutation_tests.py to feed to EngineAssetCheck and prove it fails.
internal static class Program
{
@ -122,12 +122,12 @@ internal static class Program
{
var stored = cache.AllStoredDocuments.First(s =>
s.Descriptor.DocumentType.Name == "HullData" &&
(string)s.Descriptor.DocumentType.GetField("Name").GetValue(s.Document) == "LonginusX");
(string)s.Descriptor.DocumentType.GetField("Name").GetValue(s.Document) == "Longinus");
var field = stored.Descriptor.DocumentType.GetField("Prefab", BindingFlags.Public | BindingFlags.Instance);
field.SetValue(stored.Document, newValue);
cache.UpsertAsync(stored.Descriptor.DocumentType, stored.Document, stored.Key).Wait();
cache.FlushAllBackingStores();
Console.WriteLine($"Set HullData \"LonginusX\".Prefab = \"{newValue}\"");
Console.WriteLine($"Set HullData \"Longinus\".Prefab = \"{newValue}\"");
return 0;
}
finally
@ -217,11 +217,11 @@ def main():
if code == 0:
failures.append(f"{name}: EngineAssetCheck exited 0; the mutation was not caught")
continue
if "LonginusX" not in text:
failures.append(f"{name}: failure did not name the record (LonginusX)")
if "Longinus" not in text:
failures.append(f"{name}: failure did not name the record (Longinus)")
continue
assert_contains(text, expect, name)
print(f"OK: caught, exit {code}, named LonginusX, mentioned {expect!r}")
print(f"OK: caught, exit {code}, named Longinus, mentioned {expect!r}")
except SystemExit as exc:
failures.append(f"{name}: {exc}")
finally:

View file

@ -33,7 +33,7 @@ HARDPOINT_MEMBERS = ("Type", "Position", "Shape", "Transform", "Rotation", "Armo
ANCHOR_MEMBERS = ("Id", "Role", "ModelNodeId", "ParentId", "Order")
# Enum member names in value order (HardpointType, ItemRotation).
HARDPOINT_TYPE_NAMES = ("Hull", "Tool", "Thermal", "Thruster", "WarpDrive", "Reactor", "Radiator",
"Shield", "Sensors", "Energy", "Ballistic", "Launcher", "ControlModule", "AetherDrive")
"Shield", "Sensors", "Energy", "Ballistic", "Launcher", "ControlModule")
ROTATION_NAMES = ("None", "CounterClockwise", "Reversed", "Clockwise")