namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { private static bool AdvanceTimedAction(ShipRuntime ship, float deltaSeconds, float requiredSeconds) { ship.ActionTimer += deltaSeconds; if (ship.ActionTimer < requiredSeconds) { return false; } ship.ActionTimer = 0f; return true; } internal static float GetShipCargoAmount(ShipRuntime ship) { var cargoItemId = ship.Definition.CargoItemId; return cargoItemId is null ? 0f : GetInventoryAmount(ship.Inventory, cargoItemId); } private string UpdateExtract(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { var task = ship.ControllerTask; var node = world.Nodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId); if (node is null || task.TargetPosition is null || !CanExtractNode(ship, node)) { ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } ship.TargetPosition = task.TargetPosition.Value; var distance = ship.Position.DistanceTo(task.TargetPosition.Value); if (distance > task.Threshold) { ship.ActionTimer = 0f; if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) { ship.State = "power-starved"; ship.TargetPosition = ship.Position; return "none"; } ship.State = "mining-approach"; ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, ship.Definition.Speed * deltaSeconds); return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) { ship.State = "power-starved"; ship.TargetPosition = ship.Position; return "none"; } ship.State = "mining"; if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.MiningCycleSeconds)) { return "none"; } var cargoAmount = GetShipCargoAmount(ship); var mined = MathF.Min(world.Balance.MiningRate, ship.Definition.CargoCapacity - cargoAmount); mined = MathF.Min(mined, node.OreRemaining); if (ship.Definition.CargoItemId is not null) { AddInventory(ship.Inventory, ship.Definition.CargoItemId, mined); } node.OreRemaining -= mined; if (node.OreRemaining <= 0f) { node.OreRemaining = node.MaxOre; } return GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "cargo-full" : "none"; } private string UpdateDock(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { var task = ship.ControllerTask; var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId); if (station is null || task.TargetPosition is null) { ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } var padIndex = ship.AssignedDockingPadIndex ?? ReserveDockingPad(station, ship.Id); if (padIndex is null) { ship.ActionTimer = 0f; ship.State = "awaiting-dock"; ship.TargetPosition = GetDockingHoldPosition(station, ship.Id); var waitDistance = ship.Position.DistanceTo(ship.TargetPosition); if (waitDistance > 4f && TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) { ship.Position = ship.Position.MoveToward(ship.TargetPosition, ship.Definition.Speed * deltaSeconds); } return "none"; } ship.AssignedDockingPadIndex = padIndex; var padPosition = GetDockingPadPosition(station, padIndex.Value); ship.TargetPosition = padPosition; var distance = ship.Position.DistanceTo(padPosition); if (distance > 4f) { ship.ActionTimer = 0f; if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) { ship.State = "power-starved"; ship.TargetPosition = ship.Position; return "none"; } ship.State = "docking-approach"; ship.Position = ship.Position.MoveToward(padPosition, ship.Definition.Speed * deltaSeconds); return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) { ship.State = "power-starved"; ship.TargetPosition = ship.Position; return "none"; } if (!TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) { ship.State = "power-starved"; ship.TargetPosition = ship.Position; return "none"; } ship.State = "docking"; if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.DockingDuration)) { return "none"; } ship.State = "docked"; ship.DockedStationId = station.Id; station.DockedShipIds.Add(ship.Id); ship.Position = padPosition; ship.TargetPosition = padPosition; return "docked"; } private string UpdateUnload(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { if (ship.DockedStationId is null) { ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); if (station is null) { ship.DockedStationId = null; ship.AssignedDockingPadIndex = null; ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) || !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) { ship.State = "power-starved"; ship.TargetPosition = ship.Position; return "none"; } ship.TargetPosition = GetShipDockedPosition(ship, station); ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = "transferring"; var cargoItemId = ship.Definition.CargoItemId; var moved = cargoItemId is null ? 0f : MathF.Min(GetInventoryAmount(ship.Inventory, cargoItemId), world.Balance.TransferRate * deltaSeconds); if (cargoItemId is not null) { var accepted = TryAddStationInventory(world, station, cargoItemId, moved); RemoveInventory(ship.Inventory, cargoItemId, accepted); moved = accepted; } var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId); if (faction is not null && cargoItemId == "ore") { faction.OreMined += moved; faction.Credits += moved * 0.4f; } return cargoItemId is null || GetInventoryAmount(ship.Inventory, cargoItemId) <= 0.01f ? "unloaded" : "none"; } private string UpdateRefuel(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { if (ship.DockedStationId is null) { ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); if (station is null) { ship.DockedStationId = null; ship.AssignedDockingPadIndex = null; ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) || !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) { ship.State = "power-starved"; ship.TargetPosition = ship.Position; return "none"; } ship.TargetPosition = GetShipDockedPosition(ship, station); ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = "refueling"; var transfer = MathF.Min(world.Balance.TransferRate * deltaSeconds, GetShipFuelCapacity(ship) - GetInventoryAmount(ship.Inventory, "fuel")); var moved = MathF.Min(transfer, GetInventoryAmount(station.Inventory, "fuel")); if (moved > 0.01f) { RemoveInventory(station.Inventory, "fuel", moved); AddInventory(ship.Inventory, "fuel", moved); } return !NeedsRefuel(ship) ? "refueled" : "none"; } private string UpdateConstructModule(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { if (ship.DockedStationId is null || ship.DefaultBehavior.ModuleId is null) { ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); if (station is null || !world.ModuleRecipes.TryGetValue(ship.DefaultBehavior.ModuleId, out var recipe)) { ship.AssignedDockingPadIndex = null; ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) || !TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds)) { ship.State = "power-starved"; ship.TargetPosition = ship.Position; return "none"; } if (!TryEnsureModuleConstructionStarted(station, recipe, ship.Id)) { ship.ActionTimer = 0f; ship.State = "waiting-materials"; ship.TargetPosition = GetShipDockedPosition(ship, station); return "none"; } if (station.ActiveConstruction?.AssignedConstructorShipId != ship.Id) { ship.State = "construction-blocked"; ship.TargetPosition = GetShipDockedPosition(ship, station); return "none"; } ship.TargetPosition = GetShipDockedPosition(ship, station); ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = "constructing"; station.ActiveConstruction.ProgressSeconds += deltaSeconds; if (station.ActiveConstruction.ProgressSeconds < station.ActiveConstruction.RequiredSeconds) { return "none"; } station.InstalledModules.Add(station.ActiveConstruction.ModuleId); station.ActiveConstruction = null; return "module-constructed"; } private string UpdateDeliverConstruction(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { if (ship.DockedStationId is null) { ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId); if (station is null || site is null || site.State != ConstructionSiteStateKinds.Active) { ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) || !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) { ship.State = "power-starved"; ship.TargetPosition = ship.Position; return "none"; } ship.TargetPosition = GetShipDockedPosition(ship, station); ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = "delivering-construction"; foreach (var required in site.RequiredItems) { var delivered = GetInventoryAmount(site.DeliveredItems, required.Key); var remaining = MathF.Max(0f, required.Value - delivered); if (remaining <= 0.01f) { continue; } var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds); moved = MathF.Min(moved, GetInventoryAmount(station.Inventory, required.Key)); if (moved <= 0.01f) { continue; } RemoveInventory(station.Inventory, required.Key, moved); AddInventory(site.Inventory, required.Key, moved); AddInventory(site.DeliveredItems, required.Key, moved); return IsConstructionSiteReady(site) ? "construction-delivered" : "none"; } return IsConstructionSiteReady(site) ? "construction-delivered" : "none"; } private string UpdateBuildConstructionSite(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { if (ship.DockedStationId is null) { ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId); if (station is null || site is null || site.BlueprintId is null || site.State != ConstructionSiteStateKinds.Active) { ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } if (!IsConstructionSiteReady(site) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe)) { ship.State = "waiting-materials"; ship.TargetPosition = GetShipDockedPosition(ship, station); return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) || !TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds)) { ship.State = "power-starved"; ship.TargetPosition = ship.Position; return "none"; } ship.TargetPosition = GetShipDockedPosition(ship, station); ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = "constructing"; site.AssignedConstructorShipIds.Add(ship.Id); site.Progress += deltaSeconds; if (site.Progress < recipe.Duration) { return "none"; } station.InstalledModules.Add(site.BlueprintId); PrepareNextConstructionSiteStep(world, station, site); return "site-constructed"; } private string UpdateLoadWorkers(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { if (ship.DockedStationId is null || !CanTransportWorkers(ship)) { ship.State = "blocked"; return "failed"; } var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); if (station is null || station.Population <= 0.01f) { ship.State = "idle"; return "none"; } var transfer = MathF.Min(station.Population, GetWorkerTransportCapacity(ship) - ship.WorkerPopulation); transfer = MathF.Min(transfer, 4f * deltaSeconds); if (transfer <= 0.01f) { return "none"; } station.Population = MathF.Max(0f, station.Population - transfer); ship.WorkerPopulation += transfer; ship.State = "loading"; return ship.WorkerPopulation >= GetWorkerTransportCapacity(ship) - 0.01f ? "workers-loaded" : "none"; } private string UpdateUnloadWorkers(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { if (ship.DockedStationId is null || !CanTransportWorkers(ship)) { ship.State = "blocked"; return "failed"; } var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); if (station is null || ship.WorkerPopulation <= 0.01f) { ship.State = "idle"; return "none"; } var transfer = MathF.Min(ship.WorkerPopulation, MathF.Max(0f, station.PopulationCapacity - station.Population)); transfer = MathF.Min(transfer, 4f * deltaSeconds); if (transfer <= 0.01f) { return "none"; } ship.WorkerPopulation = MathF.Max(0f, ship.WorkerPopulation - transfer); station.Population = MathF.Min(station.PopulationCapacity, station.Population + transfer); ship.State = "unloading"; return ship.WorkerPopulation <= 0.01f ? "workers-unloaded" : "none"; } private string UpdateUndock(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { var task = ship.ControllerTask; if (ship.DockedStationId is null || task.TargetPosition is null) { ship.State = "idle"; ship.TargetPosition = ship.Position; return "none"; } var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); var undockTarget = station is null ? task.TargetPosition.Value : GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, world.Balance.UndockDistance); ship.TargetPosition = undockTarget; if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) { ship.State = "power-starved"; ship.TargetPosition = ship.Position; return "none"; } if (station is not null && !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) { ship.State = "power-starved"; ship.TargetPosition = ship.Position; return "none"; } ship.State = "undocking"; if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.UndockingDuration)) { if (station is not null) { ship.Position = GetShipDockedPosition(ship, station); } return "none"; } ship.Position = ship.Position.MoveToward(undockTarget, world.Balance.UndockDistance); if (ship.Position.DistanceTo(undockTarget) > task.Threshold) { return "none"; } if (station is not null) { station.DockedShipIds.Remove(ship.Id); ReleaseDockingPad(station, ship.Id); } ship.DockedStationId = null; ship.AssignedDockingPadIndex = null; return "undocked"; } }