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; } private static void BeginTrackedAction(ShipRuntime ship, string actionKey, float total) { if (ship.TrackedActionKey == actionKey) { return; } ship.TrackedActionKey = actionKey; ship.TrackedActionTotal = MathF.Max(total, 0.01f); } 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 = ShipState.Idle; ship.TargetPosition = ship.Position; return "none"; } var cargoAmount = GetShipCargoAmount(ship); if (cargoAmount >= ship.Definition.CargoCapacity - 0.01f) { ship.ActionTimer = 0f; ship.State = ShipState.CargoFull; ship.TargetPosition = ship.Position; return "cargo-full"; } 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 = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } ship.State = ShipState.MiningApproach; ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, ship.Definition.Speed * deltaSeconds); return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) { ship.State = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } ship.State = ShipState.Mining; if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.MiningCycleSeconds)) { return "none"; } var remainingCapacity = MathF.Max(0f, ship.Definition.CargoCapacity - cargoAmount); var mined = MathF.Min(world.Balance.MiningRate, remainingCapacity); mined = MathF.Min(mined, node.OreRemaining); if (mined <= 0.01f) { ship.ActionTimer = 0f; ship.State = node.OreRemaining <= 0.01f ? ShipState.NodeDepleted : ShipState.CargoFull; ship.TargetPosition = ship.Position; return node.OreRemaining <= 0.01f ? "node-depleted" : "cargo-full"; } if (ship.Definition.CargoItemId is not null) { AddInventory(ship.Inventory, ship.Definition.CargoItemId, mined); } node.OreRemaining -= mined; node.OreRemaining = MathF.Max(0f, node.OreRemaining); 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 = ShipState.Idle; ship.TargetPosition = ship.Position; return "none"; } var padIndex = ship.AssignedDockingPadIndex ?? ReserveDockingPad(station, ship.Id); if (padIndex is null) { ship.ActionTimer = 0f; ship.State = ShipState.AwaitingDock; 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 = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } ship.State = ShipState.DockingApproach; ship.Position = ship.Position.MoveToward(padPosition, ship.Definition.Speed * deltaSeconds); return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) { ship.State = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } if (!TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) { ship.State = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } ship.State = ShipState.Docking; if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.DockingDuration)) { return "none"; } ship.State = ShipState.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 = ShipState.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 = ShipState.Idle; ship.TargetPosition = ship.Position; return "none"; } if (!TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) { ship.State = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } ship.TargetPosition = GetShipDockedPosition(ship, station); ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = ShipState.Transferring; BeginTrackedAction(ship, "transferring", GetShipCargoAmount(ship)); 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 UpdateLoadCargo(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { if (ship.DockedStationId is null) { ship.State = ShipState.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 = ShipState.Idle; ship.TargetPosition = ship.Position; return "none"; } if (!TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) { ship.State = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } ship.TargetPosition = GetShipDockedPosition(ship, station); ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = ShipState.Loading; BeginTrackedAction(ship, "loading", MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship))); var cargoItemId = ship.Definition.CargoItemId; var transfer = MathF.Min(world.Balance.TransferRate * deltaSeconds, ship.Definition.CargoCapacity - GetShipCargoAmount(ship)); var moved = cargoItemId is null ? 0f : MathF.Min(transfer, GetInventoryAmount(station.Inventory, cargoItemId)); if (cargoItemId is not null && moved > 0.01f) { RemoveInventory(station.Inventory, cargoItemId, moved); AddInventory(ship.Inventory, cargoItemId, moved); } return cargoItemId is null || GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f || GetInventoryAmount(station.Inventory, cargoItemId) <= 0.01f ? "loaded" : "none"; } private string UpdateRefuel(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { var station = ResolveShipSupportStation(ship, world); if (station is null) { ship.State = ShipState.Idle; ship.TargetPosition = ship.Position; return "none"; } var supportPosition = ResolveShipSupportPosition(ship, station); if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold)) { ship.State = ShipState.LocalFlight; ship.TargetPosition = supportPosition; ship.Position = ship.Position.MoveToward(supportPosition, ship.Definition.Speed * deltaSeconds); return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) || !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) { ship.State = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } ship.TargetPosition = supportPosition; ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = ShipState.Refueling; var refuelTarget = GetShipRefuelTarget(ship, world); BeginTrackedAction(ship, "refueling", MathF.Max(0f, refuelTarget - GetInventoryAmount(ship.Inventory, "fuel"))); var transfer = MathF.Min(world.Balance.TransferRate * deltaSeconds, refuelTarget - 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, world) ? "refueled" : "none"; } private string UpdateConstructModule(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { var station = ResolveShipSupportStation(ship, world); if (station is null || ship.DefaultBehavior.ModuleId is null) { ship.State = ShipState.Idle; ship.TargetPosition = ship.Position; return "none"; } if (!world.ModuleRecipes.TryGetValue(ship.DefaultBehavior.ModuleId, out var recipe)) { ship.AssignedDockingPadIndex = null; ship.State = ShipState.Idle; ship.TargetPosition = ship.Position; return "none"; } var supportPosition = ResolveShipSupportPosition(ship, station); if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold)) { ship.State = ShipState.LocalFlight; ship.TargetPosition = supportPosition; ship.Position = ship.Position.MoveToward(supportPosition, ship.Definition.Speed * deltaSeconds); return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) || !TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds)) { ship.State = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } if (!TryEnsureModuleConstructionStarted(station, recipe, ship.Id)) { ship.ActionTimer = 0f; ship.State = ShipState.WaitingMaterials; ship.TargetPosition = supportPosition; return "none"; } if (station.ActiveConstruction?.AssignedConstructorShipId != ship.Id) { ship.State = ShipState.ConstructionBlocked; ship.TargetPosition = supportPosition; return "none"; } ship.TargetPosition = supportPosition; ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = ShipState.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) { var station = ResolveShipSupportStation(ship, world); if (station is null) { ship.State = ShipState.Idle; ship.TargetPosition = ship.Position; return "none"; } var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId); if (station is null || site is null || site.State != ConstructionSiteStateKinds.Active) { ship.State = ShipState.Idle; ship.TargetPosition = ship.Position; return "none"; } var supportPosition = ResolveShipSupportPosition(ship, station); if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold)) { ship.State = ShipState.LocalFlight; ship.TargetPosition = supportPosition; ship.Position = ship.Position.MoveToward(supportPosition, ship.Definition.Speed * deltaSeconds); return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) || !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) { ship.State = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } ship.TargetPosition = supportPosition; ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = ShipState.DeliveringConstruction; BeginTrackedAction(ship, "delivering-construction", GetRemainingConstructionDelivery(world, site)); if (site.StationId is not null) { return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none"; } 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(world, site) ? "construction-delivered" : "none"; } return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none"; } private string UpdateBuildConstructionSite(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { var station = ResolveShipSupportStation(ship, world); if (station is null) { ship.State = ShipState.Idle; ship.TargetPosition = ship.Position; return "none"; } 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 = ShipState.Idle; ship.TargetPosition = ship.Position; return "none"; } var supportPosition = ResolveShipSupportPosition(ship, station); if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold)) { ship.State = ShipState.LocalFlight; ship.TargetPosition = supportPosition; ship.Position = ship.Position.MoveToward(supportPosition, ship.Definition.Speed * deltaSeconds); return "none"; } if (!IsConstructionSiteReady(world, site) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe)) { ship.State = ShipState.WaitingMaterials; ship.TargetPosition = supportPosition; return "none"; } if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) || !TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds)) { ship.State = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } ship.TargetPosition = supportPosition; ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = ShipState.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 StationRuntime? ResolveShipSupportStation(ShipRuntime ship, SimulationWorld world) => ship.DockedStationId is not null ? world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId) : ship.DefaultBehavior.Kind == "construct-station" && ship.DefaultBehavior.StationId is not null ? world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DefaultBehavior.StationId) : null; private static Vector3 ResolveShipSupportPosition(ShipRuntime ship, StationRuntime station) => ship.DockedStationId is not null ? GetShipDockedPosition(ship, station) : GetConstructionHoldPosition(station, ship.Id); private static bool IsShipWithinSupportRange(ShipRuntime ship, Vector3 supportPosition, float threshold) => ship.Position.DistanceTo(supportPosition) <= MathF.Max(threshold, 6f); private string UpdateLoadWorkers(ShipRuntime ship, SimulationWorld world, float deltaSeconds) { if (ship.DockedStationId is null || !CanTransportWorkers(ship)) { ship.State = ShipState.Blocked; return "failed"; } var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); if (station is null || station.Population <= 0.01f) { ship.State = ShipState.Idle; return "none"; } var transfer = MathF.Min(station.Population, GetWorkerTransportCapacity(ship) - ship.WorkerPopulation); var totalTransfer = 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 = ShipState.Loading; BeginTrackedAction(ship, "loading", totalTransfer); 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 = ShipState.Blocked; return "failed"; } var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); if (station is null || ship.WorkerPopulation <= 0.01f) { ship.State = ShipState.Idle; return "none"; } var transfer = MathF.Min(ship.WorkerPopulation, MathF.Max(0f, station.PopulationCapacity - station.Population)); var totalTransfer = transfer; 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 = ShipState.Unloading; BeginTrackedAction(ship, "unloading", totalTransfer); 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 = ShipState.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 = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } if (station is not null && !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) { ship.State = ShipState.CapacitorStarved; ship.TargetPosition = ship.Position; return "none"; } ship.State = ShipState.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"; } private static float GetRemainingConstructionDelivery(SimulationWorld world, ConstructionSiteRuntime site) => site.RequiredItems.Sum(required => MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key))); }