Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/EDNexus.App/ViewModels/Cards/RouteCardViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,10 @@ private static int RestockCount(RoutePlan plan)
}

/// <summary>Rough unladen jump range for the mode hint (full tank, no cargo, incl. Guardian bonus).</summary>
private static double ShipRange(ShipFsdProfile f) => f.JumpRangeAt(f.BaseMass + Math.Min(f.MaxFuelPerJump, f.TankSize)) + f.RangeBoost;
// The game's own figure when the Loadout carried one (it includes a Guardian booster); the drive model otherwise.
private static double ShipRange(ShipFsdProfile f) => f.MaxJumpRange > 0
? f.MaxJumpRange
: f.JumpRangeAt(f.BaseMass + Math.Min(f.MaxFuelPerJump, f.TankSize)) + f.RangeBoost;

[RelayCommand]
private async Task CopyNextSystem()
Expand Down
14 changes: 8 additions & 6 deletions src/EDNexus.Core/Ship/ShipFsdProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,23 @@ public sealed record ShipFsdProfile(
RangeBoost: rangeBoost,
CargoCapacity: loadout.GetDouble("CargoCapacity") ?? 0,
// The game's own headline figure for this build. Taken verbatim rather than derived:
// JumpRangeAt() below models the drive for the route plotter and deliberately ignores the
// Guardian booster, so it is not the number to show a commander.
// JumpRangeAt() below models the drive without the Guardian booster, so for a boosted build
// it is not the number to show a commander.
MaxJumpRange: loadout.GetDouble("MaxJumpRange") ?? 0);
}

/// <summary>
/// The unladen jump range this drive gives at the given mass — the standard FSD formula. Used to
/// sanity-check extraction against the journal's own <c>MaxJumpRange</c>. Excludes the Guardian
/// booster, whose in-game bonus is non-linear and modelled server-side by Spansh.
/// The jump range this drive gives at the given mass — the standard FSD formula,
/// <c>optimalMass / mass × (fuel / fuelMultiplier)^(1 / fuelPower)</c>, with <c>fuel</c> the most one
/// jump may burn. At <c>BaseMass + MaxFuelPerJump</c> it reproduces the journal's own
/// <c>MaxJumpRange</c> exactly for any build without a Guardian booster. Excludes that booster,
/// whose in-game bonus is non-linear and modelled server-side by Spansh.
/// </summary>
public double JumpRangeAt(double mass)
{
if (mass <= 0 || FuelMultiplier <= 0 || FuelPower <= 0) return 0;
var fuel = Math.Min(MaxFuelPerJump, TankSize);
return OptimalMass / mass * Math.Pow(1000 * fuel / FuelMultiplier, 1 / FuelPower);
return OptimalMass / mass * Math.Pow(fuel / FuelMultiplier, 1 / FuelPower);
}

/// <summary>Read the engineered FSDOptimalMass / MaxFuelPerJump overrides the journal records on the module.</summary>
Expand Down
7 changes: 3 additions & 4 deletions src/EDNexus.Core/Twitch/StreamCardMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,9 @@ private static StreamCardShip MapShip(CommanderState state) =>
/// until one has been seen.
/// </summary>
/// <remarks>
/// Deliberately not derived from <see cref="Ship.ShipFsdProfile.JumpRangeAt"/>: that models the
/// drive for the route plotter and excludes the Guardian booster, so it does not agree with the
/// figure on the commander's own ship panel — and a viewer comparing the two would see the card
/// as simply wrong.
/// Deliberately not derived from <see cref="Ship.ShipFsdProfile.JumpRangeAt"/>: that excludes the
/// Guardian booster, so for a boosted build it does not agree with the figure on the commander's
/// own ship panel — and a viewer comparing the two would see the card as simply wrong.
/// </remarks>
private static double? JumpRange(CommanderState state) =>
state.Fsd is { MaxJumpRange: > 0 } fsd ? Math.Round(fsd.MaxJumpRange, 2) : null;
Expand Down
24 changes: 22 additions & 2 deletions tests/EDNexus.Tests/Ship/ShipFsdProfileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,30 @@ public void Jump_range_falls_with_mass_and_matches_the_formula()
OptimalMass: 1050, BaseMass: 280, TankSize: 32, ReserveSize: 0.63,
FuelMultiplier: 0.012, FuelPower: 2.45, MaxFuelPerJump: 5, RangeBoost: 0, CargoCapacity: 0);

// range = optmass/mass * (1000*fuel/mul)^(1/power); fuel = min(maxfuel, tank) = 5.
var expectedAt400 = 1050.0 / 400.0 * System.Math.Pow(1000 * 5 / 0.012, 1 / 2.45);
// range = optmass/mass * (fuel/mul)^(1/power); fuel = min(maxfuel, tank) = 5.
var expectedAt400 = 1050.0 / 400.0 * System.Math.Pow(5 / 0.012, 1 / 2.45);
Assert.Equal(expectedAt400, fsd.JumpRangeAt(400), 3);
Assert.InRange(fsd.JumpRangeAt(400), 25, 35); // a 5A on a mid-weight ship: tens of ly, not hundreds

Assert.True(fsd.JumpRangeAt(300) > fsd.JumpRangeAt(500)); // heavier ships jump shorter
}

[Fact]
public void Drive_model_reproduces_the_range_the_game_reports_for_a_real_build()
{
// An engineered long-range SCO drive on an Anaconda Mk II, exactly as a recent Loadout wrote it.
// The game's headline range is the unladen ship plus the fuel for one jump.
var e = Loadout("""
{ "event": "Loadout", "Ship": "anacondamkii", "UnladenMass": 2253.880127, "MaxJumpRange": 34.576313,
"FuelCapacity": { "Main": 64.0, "Reserve": 1.08 },
"Modules": [ { "Slot": "FrameShiftDrive", "Item": "int_hyperdrive_overcharge_size8_class5", "On": true,
"Engineering": { "BlueprintName": "FSD_LongRange", "Level": 3, "Modifiers": [
{ "Label": "Mass", "Value": 192.0, "OriginalValue": 160.0 },
{ "Label": "FSDOptimalMass", "Value": 6187.75, "OriginalValue": 4670.0 } ] } } ] }
""");

var fsd = ShipFsdProfile.FromLoadout(e)!;

Assert.Equal(34.576313, fsd.JumpRangeAt(fsd.BaseMass + fsd.MaxFuelPerJump), 3);
}
}
6 changes: 3 additions & 3 deletions tests/EDNexus.Tests/Twitch/StreamCardMapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,9 @@ public void A_full_card_stays_inside_the_five_kibibyte_pubsub_cap()
[Fact]
public void Jump_range_is_the_figure_the_game_reports_not_a_derived_one()
{
// Real values off an Anaconda's Loadout: the game says 25.05 ly, while the route plotter's
// drive model (JumpRangeAt) answers in the hundreds because it is solving a different
// problem. A viewer comparing the card to the commander's ship panel must see 25.05.
// The card shows what the Loadout reported (25.05 ly here), not a figure re-derived from the
// drive model — which leaves out a Guardian booster. A viewer comparing the card to the
// commander's ship panel must see 25.05.
var state = new CommanderState
{
Ship = "anaconda",
Expand Down
Loading