563 lines
25 KiB
C#
563 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 ftlDistance = fromPosition.DistanceTo(destinationEntryPosition);
|
|
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 <= 120f)
|
|
{
|
|
var localDuration = distance / MathF.Max(ship.Definition.Speed, 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(ship.Definition.Speed, 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);
|
|
}
|