using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService; using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; namespace SpaceGame.Api.Ships.Simulation; internal sealed partial class ShipTaskExecutionService { 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) => SimulationRuntimeSupport.GetShipCargoAmount(ship); 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, world)) { 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; ship.State = ShipState.MiningApproach; ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, GetLocalTravelSpeed(ship) * deltaSeconds); 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"; } AddInventory(ship.Inventory, node.ItemId, 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) { ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * 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; ship.State = ShipState.DockingApproach; ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds); 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"; } ship.TargetPosition = GetShipDockedPosition(ship, station); ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = ShipState.Transferring; BeginTrackedAction(ship, "transferring", GetShipCargoAmount(ship)); var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId); foreach (var (itemId, amount) in ship.Inventory.ToList()) { var moved = MathF.Min(amount, world.Balance.TransferRate * deltaSeconds); var accepted = TryAddStationInventory(world, station, itemId, moved); RemoveInventory(ship.Inventory, itemId, accepted); if (faction is not null && string.Equals(itemId, "ore", StringComparison.Ordinal)) { faction.OreMined += accepted; faction.Credits += accepted * 0.4f; } } return GetShipCargoAmount(ship) <= 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"; } ship.TargetPosition = GetShipDockedPosition(ship, station); ship.Position = ship.TargetPosition; ship.ActionTimer = 0f; ship.State = ShipState.Loading; var itemId = ship.ControllerTask.ItemId; BeginTrackedAction(ship, "loading", MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship))); var transfer = MathF.Min(world.Balance.TransferRate * deltaSeconds, ship.Definition.CargoCapacity - GetShipCargoAmount(ship)); var moved = itemId is null ? 0f : MathF.Min(transfer, GetInventoryAmount(station.Inventory, itemId)); if (itemId is not null && moved > 0.01f) { RemoveInventory(station.Inventory, itemId, moved); AddInventory(ship.Inventory, itemId, moved); } return itemId is null || GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f || GetInventoryAmount(station.Inventory, itemId) <= 0.01f ? "loaded" : "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, GetLocalTravelSpeed(ship) * deltaSeconds); 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"; } AddStationModule(world, station, 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, GetLocalTravelSpeed(ship) * deltaSeconds); 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, GetLocalTravelSpeed(ship) * deltaSeconds); return "none"; } if (!IsConstructionSiteReady(world, site) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe)) { ship.State = ShipState.WaitingMaterials; ship.TargetPosition = supportPosition; 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"; } AddStationModule(world, station, 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 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; 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"; } internal static float GetRemainingConstructionDelivery(SimulationWorld world, ConstructionSiteRuntime site) => site.RequiredItems.Sum(required => MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key))); }