using SpaceGame.Simulation.Api.Contracts; using SpaceGame.Simulation.Api.Data; namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { 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 events) { foreach (var station in world.Stations) { var previousEnergy = station.EnergyStored; GenerateStationEnergy(station, 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 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, 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 || 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 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 modules, string moduleId) => modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal)); private static float GetInventoryAmount(IReadOnlyDictionary inventory, string itemId) => inventory.TryGetValue(itemId, out var amount) ? amount : 0f; private static void AddInventory(IDictionary inventory, string itemId, float amount) { if (amount <= 0f) { return; } inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary)inventory, itemId) + amount; } private static float RemoveInventory(IDictionary inventory, string itemId, float amount) { var current = GetInventoryAmount((IReadOnlyDictionary)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 float GetShipFuelCapacity(ShipRuntime ship) => CountModules(ship.Definition.Modules, "reactor-core") * ShipFuelPerReactor; internal static bool NeedsRefuel(ShipRuntime ship) => GetInventoryAmount(ship.Inventory, "fuel") < (GetShipFuelCapacity(ship) * 0.7f); 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; } if (!station.Definition.Storage.TryGetValue(storageClass, out var capacity)) { 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 bool IsConstructionSiteReady(ConstructionSiteRuntime site) => site.RequiredItems.All(entry => GetInventoryAmount(site.DeliveredItems, entry.Key) + 0.001f >= entry.Value); }