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:
parent
4d664b537f
commit
bf17314765
17 changed files with 79 additions and 145 deletions
BIN
Assets/Resources/Settings.asset
(Stored with Git LFS)
BIN
Assets/Resources/Settings.asset
(Stored with Git LFS)
Binary file not shown.
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue