Replace arbitrary game units with real-world measurements throughout the simulation and viewer: planet orbits in AU, sizes in km, galaxy positions in light-years. Add SimulationUnits helpers for conversions, separate WarpSpeed from FtlSpeed for ships, fix FTL transit progress to use galaxy-space distances, overhaul Lagrange point placement with Hill sphere approximation, and update the viewer to scale and format all distances correctly. Ships in FTL transit now render in galaxy view. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
565 lines
25 KiB
C#
565 lines
25 KiB
C#
using SpaceGame.Simulation.Api.Contracts;
|
|
using SpaceGame.Simulation.Api.Data;
|
|
|
|
namespace SpaceGame.Simulation.Api.Simulation;
|
|
|
|
public sealed partial class SimulationEngine
|
|
{
|
|
private const float StationEnergyCellToEnergyRatio = 1f;
|
|
|
|
private static bool HasShipModules(ShipDefinition definition, params string[] modules) =>
|
|
modules.All(moduleId => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
|
|
|
|
private static bool CanTransportWorkers(ShipRuntime ship) =>
|
|
CountModules(ship.Definition.Modules, "habitat-ring") > 0;
|
|
|
|
private static float GetWorkerTransportCapacity(ShipRuntime ship) =>
|
|
CountModules(ship.Definition.Modules, "habitat-ring") * 120f;
|
|
|
|
private static void UpdateStationPower(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
|
{
|
|
foreach (var station in world.Stations)
|
|
{
|
|
var previousEnergy = station.EnergyStored;
|
|
GenerateStationEnergy(station, world, deltaSeconds);
|
|
|
|
if (previousEnergy > 0.01f && station.EnergyStored <= 0.01f && GetInventoryAmount(station.Inventory, "fuel") <= 0.01f)
|
|
{
|
|
events.Add(new SimulationEventRecord("station", station.Id, "power-lost", $"{station.Definition.Label} ran out of fuel and power", DateTimeOffset.UtcNow));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void UpdateShipPower(ShipRuntime ship, SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
|
{
|
|
var previousEnergy = ship.EnergyStored;
|
|
GenerateShipEnergy(ship, world, deltaSeconds);
|
|
|
|
if (previousEnergy > 0.01f && ship.EnergyStored <= 0.01f && GetInventoryAmount(ship.Inventory, "fuel") <= 0.01f)
|
|
{
|
|
events.Add(new SimulationEventRecord("ship", ship.Id, "power-lost", $"{ship.Definition.Label} ran out of fuel and power", DateTimeOffset.UtcNow));
|
|
}
|
|
}
|
|
|
|
private static void GenerateStationEnergy(StationRuntime station, SimulationWorld world, float deltaSeconds)
|
|
{
|
|
var powerCores = CountModules(station.InstalledModules, "power-core");
|
|
var tanks = CountModules(station.InstalledModules, "liquid-tank");
|
|
if (powerCores <= 0 || tanks <= 0)
|
|
{
|
|
station.EnergyStored = 0f;
|
|
station.Inventory.Remove("fuel");
|
|
return;
|
|
}
|
|
|
|
var energyCapacity = powerCores * StationEnergyPerPowerCore;
|
|
var fuelStored = GetInventoryAmount(station.Inventory, "fuel");
|
|
var desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored);
|
|
if (desiredEnergy <= 0.01f)
|
|
{
|
|
station.EnergyStored = MathF.Min(station.EnergyStored, energyCapacity);
|
|
station.Inventory["fuel"] = MathF.Min(fuelStored, tanks * StationFuelPerTank);
|
|
return;
|
|
}
|
|
|
|
var solarGenerated = MathF.Min(desiredEnergy, GetStationSolarGeneration(station, world) * deltaSeconds);
|
|
if (solarGenerated > 0.01f)
|
|
{
|
|
station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + solarGenerated);
|
|
desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored);
|
|
}
|
|
|
|
if (desiredEnergy > 0.01f && fuelStored <= 0.01f)
|
|
{
|
|
var energyCells = GetInventoryAmount(station.Inventory, "energy-cell");
|
|
if (energyCells > 0.01f)
|
|
{
|
|
var consumedCells = MathF.Min(energyCells, desiredEnergy / StationEnergyCellToEnergyRatio);
|
|
RemoveInventory(station.Inventory, "energy-cell", consumedCells);
|
|
station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + (consumedCells * StationEnergyCellToEnergyRatio));
|
|
desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored);
|
|
}
|
|
}
|
|
|
|
if (desiredEnergy <= 0.01f || fuelStored <= 0.01f)
|
|
{
|
|
station.EnergyStored = MathF.Min(station.EnergyStored, energyCapacity);
|
|
station.Inventory["fuel"] = MathF.Min(fuelStored, tanks * StationFuelPerTank);
|
|
return;
|
|
}
|
|
|
|
var generated = MathF.Min(desiredEnergy, powerCores * 24f * deltaSeconds);
|
|
var requiredFuel = generated / StationFuelToEnergyRatio;
|
|
var consumedFuel = MathF.Min(requiredFuel, fuelStored);
|
|
var actualGenerated = consumedFuel * StationFuelToEnergyRatio;
|
|
|
|
RemoveInventory(station.Inventory, "fuel", consumedFuel);
|
|
station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + actualGenerated);
|
|
}
|
|
|
|
private static float GetStationFuelCapacity(StationRuntime station) =>
|
|
CountModules(station.InstalledModules, "liquid-tank") * StationFuelPerTank;
|
|
|
|
private static float GetStationEnergyCapacity(StationRuntime station) =>
|
|
CountModules(station.InstalledModules, "power-core") * StationEnergyPerPowerCore;
|
|
|
|
private static float GetStationSolarGeneration(StationRuntime station, SimulationWorld world) =>
|
|
world.Balance.Energy.StationSolarCharge * (1f + CountModules(station.InstalledModules, "solar-array"));
|
|
|
|
private static float GetStationStorageCapacity(StationRuntime station, string storageClass)
|
|
{
|
|
var baseCapacity = station.Definition.Storage.TryGetValue(storageClass, out var capacity)
|
|
? capacity
|
|
: 0f;
|
|
|
|
var extraBulkBays = Math.Max(0, CountModules(station.InstalledModules, "bulk-bay") - CountModules(station.Definition.Modules, "bulk-bay"));
|
|
var extraLiquidTanks = Math.Max(0, CountModules(station.InstalledModules, "liquid-tank") - CountModules(station.Definition.Modules, "liquid-tank"));
|
|
var extraGasTanks = Math.Max(0, CountModules(station.InstalledModules, "gas-tank") - CountModules(station.Definition.Modules, "gas-tank"));
|
|
var extraContainerBays = Math.Max(0, CountModules(station.InstalledModules, "container-bay") - CountModules(station.Definition.Modules, "container-bay"));
|
|
var moduleBonus = storageClass switch
|
|
{
|
|
"bulk-solid" => extraBulkBays * 1000f,
|
|
"bulk-liquid" => extraLiquidTanks * 500f,
|
|
"bulk-gas" => extraGasTanks * 500f,
|
|
"container" => extraContainerBays * 800f,
|
|
_ => 0f,
|
|
};
|
|
|
|
return baseCapacity + moduleBonus;
|
|
}
|
|
|
|
private static void GenerateShipEnergy(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
{
|
|
var reactors = CountModules(ship.Definition.Modules, "reactor-core");
|
|
var capacitors = CountModules(ship.Definition.Modules, "capacitor-bank");
|
|
if (reactors <= 0 || capacitors <= 0)
|
|
{
|
|
ship.EnergyStored = 0f;
|
|
ship.Inventory.Remove("fuel");
|
|
return;
|
|
}
|
|
|
|
var energyCapacity = capacitors * CapacitorEnergyPerModule;
|
|
var fuelCapacity = reactors * ShipFuelPerReactor;
|
|
var fuelStored = GetInventoryAmount(ship.Inventory, "fuel");
|
|
var desiredEnergy = MathF.Max(0f, energyCapacity - ship.EnergyStored);
|
|
if (desiredEnergy <= 0.01f || fuelStored <= 0.01f)
|
|
{
|
|
ship.EnergyStored = MathF.Min(ship.EnergyStored, energyCapacity);
|
|
ship.Inventory["fuel"] = MathF.Min(fuelStored, fuelCapacity);
|
|
return;
|
|
}
|
|
|
|
var generated = MathF.Min(desiredEnergy, world.Balance.Energy.ShipRechargeRate * reactors * deltaSeconds);
|
|
var requiredFuel = generated / ShipFuelToEnergyRatio;
|
|
var consumedFuel = MathF.Min(requiredFuel, fuelStored);
|
|
var actualGenerated = consumedFuel * ShipFuelToEnergyRatio;
|
|
|
|
RemoveInventory(ship.Inventory, "fuel", consumedFuel);
|
|
ship.EnergyStored = MathF.Min(energyCapacity, ship.EnergyStored + actualGenerated);
|
|
}
|
|
|
|
private static bool TryConsumeShipEnergy(ShipRuntime ship, float amount)
|
|
{
|
|
if (ship.EnergyStored + 0.0001f < amount)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ship.EnergyStored = MathF.Max(0f, ship.EnergyStored - amount);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryConsumeStationEnergy(StationRuntime station, float amount)
|
|
{
|
|
if (station.EnergyStored + 0.0001f < amount)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
station.EnergyStored = MathF.Max(0f, station.EnergyStored - amount);
|
|
return true;
|
|
}
|
|
|
|
private static int CountModules(IEnumerable<string> modules, string moduleId) =>
|
|
modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal));
|
|
|
|
private static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
|
|
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
|
|
|
|
private static void AddInventory(IDictionary<string, float> inventory, string itemId, float amount)
|
|
{
|
|
if (amount <= 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId) + amount;
|
|
}
|
|
|
|
private static float RemoveInventory(IDictionary<string, float> inventory, string itemId, float amount)
|
|
{
|
|
var current = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId);
|
|
var removed = MathF.Min(current, amount);
|
|
var remaining = current - removed;
|
|
if (remaining <= 0.001f)
|
|
{
|
|
inventory.Remove(itemId);
|
|
}
|
|
else
|
|
{
|
|
inventory[itemId] = remaining;
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
|
|
private static bool HasStationModules(StationRuntime station, params string[] modules) =>
|
|
modules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal));
|
|
|
|
private static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node) =>
|
|
node.ItemId switch
|
|
{
|
|
"ore" => HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", "mining-turret"),
|
|
"gas" => HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", "gas-extractor"),
|
|
_ => false,
|
|
};
|
|
|
|
private static bool CanBuildClaimBeacon(ShipRuntime ship) =>
|
|
string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal);
|
|
|
|
private static float GetShipFuelCapacity(ShipRuntime ship) =>
|
|
CountModules(ship.Definition.Modules, "reactor-core") * ShipFuelPerReactor;
|
|
|
|
private static float GetShipAvailableEnergyBudget(ShipRuntime ship) =>
|
|
ship.EnergyStored + (GetInventoryAmount(ship.Inventory, "fuel") * ShipFuelToEnergyRatio);
|
|
|
|
private static float GetShipFuelReserve(ShipRuntime ship, float plannedFuel)
|
|
{
|
|
var capacity = GetShipFuelCapacity(ship);
|
|
var reserveRatio = ship.Definition.CargoItemId == "gas" ? 0.4f : 0.3f;
|
|
var reserve = MathF.Max(16f, MathF.Max(capacity * 0.18f, plannedFuel * reserveRatio));
|
|
return MathF.Min(capacity, reserve);
|
|
}
|
|
|
|
private static float EstimateFuelForEnergyDemand(ShipRuntime ship, float energyDemand) =>
|
|
MathF.Max(0f, energyDemand - ship.EnergyStored) / ShipFuelToEnergyRatio;
|
|
|
|
private static float EstimateTimedEnergyUse(SimulationWorld world, float durationSeconds, float drainPerSecond) =>
|
|
MathF.Max(0f, durationSeconds) * drainPerSecond;
|
|
|
|
private static float EstimateTravelEnergy(
|
|
ShipRuntime ship,
|
|
SimulationWorld world,
|
|
Vector3 fromPosition,
|
|
string fromSystemId,
|
|
Vector3 toPosition,
|
|
string toSystemId)
|
|
{
|
|
if (!string.Equals(fromSystemId, toSystemId, StringComparison.Ordinal))
|
|
{
|
|
var destinationEntryNode = ResolveSystemEntryNode(world, toSystemId);
|
|
var destinationEntryPosition = destinationEntryNode?.Position ?? toPosition;
|
|
var originSystemPosition = ResolveSystemGalaxyPosition(world, fromSystemId);
|
|
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, toSystemId);
|
|
var ftlDistance = originSystemPosition.DistanceTo(destinationSystemPosition);
|
|
var ftlDuration = ftlDistance / MathF.Max(ship.Definition.FtlSpeed, 0.01f);
|
|
return EstimateTimedEnergyUse(world, ship.Definition.SpoolTime, world.Balance.Energy.IdleDrain)
|
|
+ EstimateTimedEnergyUse(world, ftlDuration, world.Balance.Energy.WarpDrain)
|
|
+ EstimateInSystemTravelEnergy(ship, world, destinationEntryPosition, toPosition);
|
|
}
|
|
|
|
return EstimateInSystemTravelEnergy(ship, world, fromPosition, toPosition);
|
|
}
|
|
|
|
private static float EstimateInSystemTravelEnergy(ShipRuntime ship, SimulationWorld world, Vector3 fromPosition, Vector3 toPosition)
|
|
{
|
|
var distance = fromPosition.DistanceTo(toPosition);
|
|
if (distance <= world.Balance.ArrivalThreshold)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
if (distance <= WarpEngageDistanceKilometers)
|
|
{
|
|
var localDuration = distance / MathF.Max(GetLocalTravelSpeed(ship), 0.01f);
|
|
return EstimateTimedEnergyUse(world, localDuration, world.Balance.Energy.MoveDrain);
|
|
}
|
|
|
|
var warpSpoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
|
|
var warpDuration = distance / MathF.Max(GetWarpTravelSpeed(ship), 0.01f);
|
|
return EstimateTimedEnergyUse(world, warpSpoolDuration, world.Balance.Energy.IdleDrain)
|
|
+ EstimateTimedEnergyUse(world, warpDuration, world.Balance.Energy.WarpDrain);
|
|
}
|
|
|
|
private static float EstimateDockingEnergy(SimulationWorld world) =>
|
|
EstimateTimedEnergyUse(world, world.Balance.DockingDuration, world.Balance.Energy.MoveDrain)
|
|
+ EstimateTimedEnergyUse(world, 6f, world.Balance.Energy.IdleDrain);
|
|
|
|
private static float EstimateUndockingEnergy(SimulationWorld world) =>
|
|
EstimateTimedEnergyUse(world, world.Balance.UndockingDuration, world.Balance.Energy.MoveDrain)
|
|
+ EstimateTimedEnergyUse(world, 4f, world.Balance.Energy.IdleDrain);
|
|
|
|
private static float EstimateExtractionEnergy(ShipRuntime ship, SimulationWorld world)
|
|
{
|
|
var remainingCargo = MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship));
|
|
if (remainingCargo <= 0.01f)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
var cycles = MathF.Ceiling(remainingCargo / MathF.Max(world.Balance.MiningRate, 0.01f));
|
|
return EstimateTimedEnergyUse(world, cycles * world.Balance.MiningCycleSeconds, world.Balance.Energy.MoveDrain)
|
|
+ EstimateTimedEnergyUse(world, cycles * 1.5f, world.Balance.Energy.IdleDrain);
|
|
}
|
|
|
|
private static float EstimateConstructionEnergy(ShipRuntime ship, SimulationWorld world, StationRuntime station)
|
|
{
|
|
var holdPosition = GetConstructionHoldPosition(station, ship.Id);
|
|
var travelEnergy = EstimateTravelEnergy(ship, world, ship.Position, ship.SystemId, holdPosition, station.SystemId);
|
|
var site = GetConstructionSiteForStation(world, station.Id);
|
|
if (site is not null && site.State == ConstructionSiteStateKinds.Active && IsConstructionSiteReady(world, site))
|
|
{
|
|
if (world.ModuleRecipes.TryGetValue(site.BlueprintId ?? string.Empty, out var siteRecipe))
|
|
{
|
|
return travelEnergy + EstimateTimedEnergyUse(world, siteRecipe.Duration, world.Balance.Energy.IdleDrain);
|
|
}
|
|
|
|
return travelEnergy;
|
|
}
|
|
|
|
var moduleId = site?.BlueprintId ?? GetNextStationModuleToBuild(station, world);
|
|
if (moduleId is not null
|
|
&& world.ModuleRecipes.TryGetValue(moduleId, out var recipe)
|
|
&& CanStartModuleConstruction(station, recipe))
|
|
{
|
|
return travelEnergy + EstimateTimedEnergyUse(world, recipe.Duration, world.Balance.Energy.IdleDrain);
|
|
}
|
|
|
|
return travelEnergy;
|
|
}
|
|
|
|
private static float EstimateResourceHarvestEnergy(ShipRuntime ship, SimulationWorld world)
|
|
{
|
|
var cargoItemId = ship.Definition.CargoItemId;
|
|
if (cargoItemId is null)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
var requiredModule = cargoItemId == "gas" ? "gas-extractor" : "mining-turret";
|
|
var behavior = ship.DefaultBehavior;
|
|
var refinery = SelectBestBuyStation(world, ship, cargoItemId, behavior.StationId);
|
|
var node = behavior.NodeId is null
|
|
? world.Nodes
|
|
.Where(candidate =>
|
|
(behavior.AreaSystemId is null || candidate.SystemId == behavior.AreaSystemId) &&
|
|
candidate.ItemId == cargoItemId &&
|
|
candidate.OreRemaining > 0.01f)
|
|
.OrderByDescending(candidate => candidate.OreRemaining)
|
|
.FirstOrDefault()
|
|
: world.Nodes.FirstOrDefault(candidate => candidate.Id == behavior.NodeId && candidate.OreRemaining > 0.01f);
|
|
if (refinery is null || node is null || !HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", requiredModule))
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
var currentPosition = ship.Position;
|
|
var currentSystemId = ship.SystemId;
|
|
var energy = 0f;
|
|
var cargoAmount = GetShipCargoAmount(ship);
|
|
if (ship.DockedStationId == refinery.Id)
|
|
{
|
|
currentPosition = GetUndockTargetPosition(refinery, ship.AssignedDockingPadIndex, world.Balance.UndockDistance);
|
|
currentSystemId = refinery.SystemId;
|
|
energy += EstimateUndockingEnergy(world);
|
|
}
|
|
|
|
if (cargoAmount > 0.01f)
|
|
{
|
|
energy += EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, refinery.Position, refinery.SystemId);
|
|
return energy + EstimateDockingEnergy(world);
|
|
}
|
|
|
|
var holdPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f);
|
|
energy += EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, holdPosition, node.SystemId);
|
|
energy += EstimateExtractionEnergy(ship, world);
|
|
energy += EstimateTravelEnergy(ship, world, holdPosition, node.SystemId, refinery.Position, refinery.SystemId);
|
|
energy += EstimateDockingEnergy(world);
|
|
return energy;
|
|
}
|
|
|
|
private static float EstimateResourceReturnEnergy(ShipRuntime ship, SimulationWorld world)
|
|
{
|
|
var cargoItemId = ship.Definition.CargoItemId;
|
|
if (cargoItemId is null)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
var refinery = SelectBestBuyStation(world, ship, cargoItemId, ship.DefaultBehavior.StationId);
|
|
if (refinery is null)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
var currentPosition = ship.Position;
|
|
var currentSystemId = ship.SystemId;
|
|
return EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, refinery.Position, refinery.SystemId)
|
|
+ EstimateDockingEnergy(world);
|
|
}
|
|
|
|
private static float EstimateTransportEnergy(ShipRuntime ship, SimulationWorld world)
|
|
{
|
|
var cargoItemId = ship.Definition.CargoItemId;
|
|
if (cargoItemId is null)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
var behavior = ship.DefaultBehavior;
|
|
var source = SelectBestSellStation(world, ship, cargoItemId, behavior.StationId);
|
|
var destination = SelectBestBuyStation(world, ship, cargoItemId, behavior.StationId);
|
|
if (source is null && destination is null)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
var cargoAmount = GetShipCargoAmount(ship);
|
|
var currentPosition = ship.Position;
|
|
var currentSystemId = ship.SystemId;
|
|
if (ship.DockedStationId is not null)
|
|
{
|
|
var dockedStation = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
|
|
if (dockedStation is not null)
|
|
{
|
|
currentPosition = GetUndockTargetPosition(dockedStation, ship.AssignedDockingPadIndex, world.Balance.UndockDistance);
|
|
currentSystemId = dockedStation.SystemId;
|
|
}
|
|
}
|
|
|
|
var targetStation = cargoAmount > 0.01f ? destination : source;
|
|
if (targetStation is null)
|
|
{
|
|
return ship.DockedStationId is not null ? EstimateUndockingEnergy(world) : 0f;
|
|
}
|
|
|
|
var energy = ship.DockedStationId is not null ? EstimateUndockingEnergy(world) : 0f;
|
|
energy += EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, targetStation.Position, targetStation.SystemId);
|
|
return energy + EstimateDockingEnergy(world);
|
|
}
|
|
|
|
private static float EstimateShipMissionEnergyDemand(ShipRuntime ship, SimulationWorld world) =>
|
|
ship.DefaultBehavior.Kind switch
|
|
{
|
|
"auto-mine" or "auto-harvest-gas" => EstimateResourceHarvestEnergy(ship, world),
|
|
"auto-supply-energy" => EstimateTransportEnergy(ship, world),
|
|
"construct-station" when ship.DefaultBehavior.StationId is not null
|
|
=> world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DefaultBehavior.StationId) is { } station
|
|
? EstimateConstructionEnergy(ship, world, station)
|
|
: 0f,
|
|
_ when ship.ControllerTask.TargetPosition is { } targetPosition && ship.ControllerTask.TargetSystemId is { } targetSystemId
|
|
=> EstimateTravelEnergy(ship, world, ship.Position, ship.SystemId, targetPosition, targetSystemId),
|
|
_ => 0f,
|
|
};
|
|
|
|
private static float GetShipRefuelTarget(ShipRuntime ship, SimulationWorld world)
|
|
{
|
|
var capacity = GetShipFuelCapacity(ship);
|
|
var missionFuel = EstimateFuelForEnergyDemand(ship, EstimateShipMissionEnergyDemand(ship, world));
|
|
var reserveFuel = GetShipFuelReserve(ship, missionFuel);
|
|
return MathF.Min(capacity, missionFuel + reserveFuel);
|
|
}
|
|
|
|
internal static bool NeedsRefuel(ShipRuntime ship, SimulationWorld world) =>
|
|
GetInventoryAmount(ship.Inventory, "fuel") + 0.01f < GetShipRefuelTarget(ship, world);
|
|
|
|
internal static bool NeedsEmergencyReturn(ShipRuntime ship, SimulationWorld world)
|
|
{
|
|
if (ship.DefaultBehavior.Kind is not "auto-mine" and not "auto-harvest-gas")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var returnEnergy = EstimateResourceReturnEnergy(ship, world);
|
|
var reserveFuel = GetShipFuelReserve(ship, EstimateFuelForEnergyDemand(ship, returnEnergy));
|
|
var requiredBudget = returnEnergy + (reserveFuel * ShipFuelToEnergyRatio);
|
|
return GetShipAvailableEnergyBudget(ship) + 0.01f < requiredBudget;
|
|
}
|
|
|
|
private static float ComputeWorkforceRatio(float population, float workforceRequired)
|
|
{
|
|
if (workforceRequired <= 0.01f)
|
|
{
|
|
return 1f;
|
|
}
|
|
|
|
var staffedRatio = MathF.Min(1f, population / workforceRequired);
|
|
return 0.1f + (0.9f * staffedRatio);
|
|
}
|
|
|
|
private static string? GetStorageRequirement(string storageClass) =>
|
|
storageClass switch
|
|
{
|
|
"bulk-solid" => "bulk-bay",
|
|
"bulk-liquid" => "liquid-tank",
|
|
"bulk-gas" => "gas-tank",
|
|
_ => null,
|
|
};
|
|
|
|
private static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount)
|
|
{
|
|
if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
var storageClass = itemDefinition.Storage;
|
|
var requiredModule = GetStorageRequirement(storageClass);
|
|
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
var capacity = GetStationStorageCapacity(station, storageClass);
|
|
if (capacity <= 0.01f)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
var used = station.Inventory
|
|
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.Storage == storageClass)
|
|
.Sum(entry => entry.Value);
|
|
var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used));
|
|
if (accepted <= 0.01f)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
AddInventory(station.Inventory, itemId, accepted);
|
|
return accepted;
|
|
}
|
|
|
|
private static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) =>
|
|
recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount);
|
|
|
|
private static ConstructionSiteRuntime? GetConstructionSiteForStation(SimulationWorld world, string stationId) =>
|
|
world.ConstructionSites.FirstOrDefault(site =>
|
|
string.Equals(site.StationId, stationId, StringComparison.Ordinal)
|
|
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed);
|
|
|
|
private static float GetConstructionDeliveredAmount(SimulationWorld world, ConstructionSiteRuntime site, string itemId)
|
|
{
|
|
if (site.StationId is not null
|
|
&& world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station)
|
|
{
|
|
return GetInventoryAmount(station.Inventory, itemId);
|
|
}
|
|
|
|
return GetInventoryAmount(site.DeliveredItems, itemId);
|
|
}
|
|
|
|
private static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) =>
|
|
site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value);
|
|
}
|