using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService; using static SpaceGame.Api.Stations.Simulation.StationSimulationService; namespace SpaceGame.Api.Ships.AI; public sealed partial class ShipAiService { private SubTaskOutcome UpdateSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { return subTask.Kind switch { var kind when string.Equals(kind, ShipTaskKinds.Travel, StringComparison.Ordinal) => UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: true), var kind when string.Equals(kind, ShipTaskKinds.FollowTarget, StringComparison.Ordinal) => UpdateFollowSubTask(world, ship, subTask, deltaSeconds), var kind when string.Equals(kind, ShipTaskKinds.Dock, StringComparison.Ordinal) => UpdateDockSubTask(world, ship, subTask, deltaSeconds), var kind when string.Equals(kind, ShipTaskKinds.Undock, StringComparison.Ordinal) => UpdateUndockSubTask(world, ship, subTask, deltaSeconds), var kind when string.Equals(kind, ShipTaskKinds.LoadCargo, StringComparison.Ordinal) => UpdateLoadCargoSubTask(world, ship, subTask, deltaSeconds), var kind when string.Equals(kind, ShipTaskKinds.UnloadCargo, StringComparison.Ordinal) => UpdateUnloadCargoSubTask(world, ship, subTask, deltaSeconds), var kind when string.Equals(kind, ShipTaskKinds.TransferCargoToShip, StringComparison.Ordinal) => UpdateTransferCargoToShipSubTask(world, ship, subTask, deltaSeconds), var kind when string.Equals(kind, ShipTaskKinds.MineNode, StringComparison.Ordinal) => UpdateMineSubTask(world, ship, subTask, deltaSeconds), var kind when string.Equals(kind, ShipTaskKinds.SalvageWreck, StringComparison.Ordinal) => UpdateSalvageSubTask(world, ship, subTask, deltaSeconds), var kind when string.Equals(kind, ShipTaskKinds.DeliverConstruction, StringComparison.Ordinal) => UpdateDeliverConstructionSubTask(world, ship, subTask, deltaSeconds), var kind when string.Equals(kind, ShipTaskKinds.BuildConstructionSite, StringComparison.Ordinal) => UpdateBuildConstructionSubTask(world, ship, subTask, deltaSeconds), var kind when string.Equals(kind, ShipTaskKinds.AttackTarget, StringComparison.Ordinal) => UpdateAttackSubTask(world, ship, subTask, deltaSeconds), var kind when string.Equals(kind, ShipTaskKinds.HoldPosition, StringComparison.Ordinal) => UpdateHoldSubTask(ship, subTask, deltaSeconds), _ => SubTaskOutcome.Failed, }; } private SubTaskOutcome UpdateHoldSubTask(ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { ship.State = ShipState.HoldingPosition; ship.TargetPosition = subTask.TargetPosition ?? ship.Position; ship.Position = ship.Position.MoveToward(ship.TargetPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, ship.Position.DistanceTo(ship.TargetPosition))); return AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.1f, subTask.Amount)) ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } private SubTaskOutcome UpdateFollowSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.Health > 0f); if (targetShip is null) { subTask.BlockingReason = "follow-target-missing"; return SubTaskOutcome.Failed; } var desiredPosition = GetFormationPosition(targetShip.Position, ship.Id, MathF.Max(subTask.Threshold, 16f)); subTask.TargetSystemId = targetShip.SystemId; subTask.TargetPosition = desiredPosition; subTask.BlockingReason = null; if (ship.SystemId != targetShip.SystemId || ship.Position.DistanceTo(desiredPosition) > MathF.Max(subTask.Threshold, 12f)) { return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false); } ship.State = ShipState.HoldingPosition; ship.TargetPosition = desiredPosition; ship.Position = ship.Position.MoveToward(desiredPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, ship.Position.DistanceTo(desiredPosition))); return AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.5f, subTask.Amount)) ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } private SubTaskOutcome UpdateTravelSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds, bool completeOnArrival) { if (subTask.TargetPosition is null || subTask.TargetSystemId is null) { subTask.BlockingReason = "travel-target-missing"; ship.State = ShipState.Blocked; return SubTaskOutcome.Failed; } var targetPosition = ResolveCurrentTargetPosition(world, subTask); var targetAnchor = ResolveTravelTargetAnchor(world, subTask, targetPosition); ship.TargetPosition = targetPosition; if (ship.SystemId != subTask.TargetSystemId) { if (!CanFtl(ship.Definition)) { subTask.BlockingReason = "ftl-unavailable"; ship.State = ShipState.Blocked; return SubTaskOutcome.Failed; } var destinationEntryAnchor = ResolveSystemEntryAnchor(world, subTask.TargetSystemId) ?? targetAnchor; var destinationEntryPosition = destinationEntryAnchor?.Position ?? targetPosition; return UpdateFtlTransit(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, destinationEntryPosition, destinationEntryAnchor, completeOnArrival, targetPosition, targetAnchor); } var currentAnchor = ResolveCurrentAnchor(world, ship); if (targetAnchor is not null && currentAnchor is not null && !string.Equals(currentAnchor.Id, targetAnchor.Id, StringComparison.Ordinal)) { if (!CanWarp(ship.Definition)) { return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, currentAnchor, targetAnchor, completeOnArrival); } return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, currentAnchor, targetAnchor, completeOnArrival); } if (targetAnchor is not null && currentAnchor is not null && !string.Equals(currentAnchor.Id, targetAnchor.Id, StringComparison.Ordinal) && CanWarp(ship.Definition)) { return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, currentAnchor, targetAnchor, completeOnArrival); } return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, currentAnchor, targetAnchor, completeOnArrival); } private SubTaskOutcome UpdateAttackSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { var hostileShip = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.Health > 0f); var hostileStation = hostileShip is null ? world.Stations.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId) : null; if ((hostileShip is not null && hostileShip.FactionId == ship.FactionId) || (hostileStation is not null && hostileStation.FactionId == ship.FactionId)) { subTask.BlockingReason = "friendly-target"; return SubTaskOutcome.Failed; } if (hostileShip is null && hostileStation is null) { return SubTaskOutcome.Completed; } var targetSystemId = hostileShip?.SystemId ?? hostileStation!.SystemId; var targetPosition = hostileShip?.Position ?? hostileStation!.Position; var attackRange = hostileShip is null ? hostileStation!.Radius + 18f : 26f; subTask.TargetSystemId = targetSystemId; subTask.TargetPosition = targetPosition; subTask.Threshold = attackRange; if (ship.SystemId != targetSystemId || ship.Position.DistanceTo(targetPosition) > attackRange) { return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false); } ship.State = ShipState.EngagingTarget; ship.TargetPosition = targetPosition; ship.Position = ship.Position.MoveToward(targetPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, 8f)); var damage = GetShipDamagePerSecond(ship) * deltaSeconds * GetSkillFactor(ship.Skills.Combat); subTask.Progress = 1f; if (hostileShip is not null) { hostileShip.Health = MathF.Max(0f, hostileShip.Health - damage); return hostileShip.Health <= 0f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } hostileStation!.Health = MathF.Max(0f, hostileStation.Health - (damage * 0.6f)); return hostileStation.Health <= 0f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } private SubTaskOutcome UpdateMineSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { var node = ResolveNode(world, subTask.TargetResourceNodeId ?? subTask.TargetEntityId); if (node is null || !CanExtractNode(ship, node, world)) { subTask.BlockingReason = "node-missing"; ship.State = ShipState.Blocked; return SubTaskOutcome.Failed; } var deposit = ResolveResourceDeposit(world, subTask.TargetResourceDepositId); if (deposit is null || !string.Equals(deposit.NodeId, node.Id, StringComparison.Ordinal) || deposit.OreRemaining <= 0.01f) { deposit = SelectMiningDeposit(node, ship.Id); subTask.TargetResourceDepositId = deposit?.Id; } if (deposit is null) { SyncNodeOreTotals(node); return SubTaskOutcome.Completed; } var targetPosition = GetResourceHoldPosition(deposit.Position, ship.Id, 20f); subTask.TargetPosition = targetPosition; var approachThreshold = MathF.Max(subTask.Threshold, 8f); var distanceToTarget = ship.Position.DistanceTo(targetPosition); var distanceToDeposit = ship.Position.DistanceTo(deposit.Position); var effectivelyAtDeposit = string.Equals(ship.SpatialState.CurrentAnchorId, node.AnchorId, StringComparison.Ordinal) && distanceToDeposit <= approachThreshold; ship.TargetPosition = targetPosition; if (distanceToTarget > approachThreshold && !effectivelyAtDeposit) { ship.State = ShipState.MiningApproach; ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); return SubTaskOutcome.Active; } var cargoAmount = GetShipCargoAmount(ship); if (cargoAmount >= ship.Definition.GetTotalCargoCapacity() - 0.01f) { return SubTaskOutcome.Completed; } ship.State = ShipState.Mining; if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.MiningCycleSeconds)) { return SubTaskOutcome.Active; } var remainingCapacity = MathF.Max(0f, ship.Definition.GetTotalCargoCapacity() - cargoAmount); var mined = MathF.Min(balance.MiningRate * GetSkillFactor(ship.Skills.Mining), remainingCapacity); mined = MathF.Min(mined, deposit.OreRemaining); if (mined <= 0.01f) { return SubTaskOutcome.Completed; } AddInventory(ship.Inventory, node.ItemId, mined); deposit.OreRemaining = MathF.Max(0f, deposit.OreRemaining - mined); SyncNodeOreTotals(node); if (GetShipCargoAmount(ship) >= ship.Definition.GetTotalCargoCapacity() - 0.01f || node.OreRemaining <= 0.01f) { return SubTaskOutcome.Completed; } subTask.ElapsedSeconds = 0f; return SubTaskOutcome.Active; } private SubTaskOutcome UpdateDockSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { var station = ResolveStation(world, subTask.TargetEntityId); if (station is null) { subTask.BlockingReason = "dock-target-missing"; ship.State = ShipState.Blocked; return SubTaskOutcome.Failed; } var padIndex = ship.AssignedDockingPadIndex ?? ReserveDockingPad(station, ship.Id); if (padIndex is null) { ship.State = ShipState.AwaitingDock; ship.TargetPosition = GetDockingHoldPosition(station, ship.Id); if (ship.Position.DistanceTo(ship.TargetPosition) > 4f) { ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); } subTask.Status = WorkStatus.Blocked; subTask.BlockingReason = "waiting-for-pad"; return SubTaskOutcome.Active; } subTask.Status = WorkStatus.Active; subTask.BlockingReason = null; ship.AssignedDockingPadIndex = padIndex; var padPosition = GetDockingPadPosition(station, padIndex.Value); ship.TargetPosition = padPosition; if (ship.Position.DistanceTo(padPosition) > 4f) { ship.State = ShipState.DockingApproach; ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds); return SubTaskOutcome.Active; } ship.State = ShipState.Docking; if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.DockingDuration)) { return SubTaskOutcome.Active; } ship.State = ShipState.Docked; ship.DockedStationId = station.Id; station.DockedShipIds.Add(ship.Id); ship.KnownStationIds.Add(station.Id); ship.Position = padPosition; ship.TargetPosition = padPosition; return SubTaskOutcome.Completed; } private SubTaskOutcome UpdateUndockSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { if (ship.DockedStationId is null) { return SubTaskOutcome.Completed; } var station = ResolveStation(world, ship.DockedStationId); if (station is null) { ship.DockedStationId = null; ship.AssignedDockingPadIndex = null; return SubTaskOutcome.Completed; } var undockTarget = GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, balance.UndockDistance); ship.TargetPosition = undockTarget; ship.State = ShipState.Undocking; if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.UndockingDuration)) { ship.Position = GetShipDockedPosition(ship, station); return SubTaskOutcome.Active; } ship.Position = ship.Position.MoveToward(undockTarget, balance.UndockDistance); if (ship.Position.DistanceTo(undockTarget) > MathF.Max(subTask.Threshold, 4f)) { return SubTaskOutcome.Active; } station.DockedShipIds.Remove(ship.Id); ReleaseDockingPad(station, ship.Id); ship.DockedStationId = null; ship.AssignedDockingPadIndex = null; return SubTaskOutcome.Completed; } private SubTaskOutcome UpdateLoadCargoSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { if (ship.DockedStationId is null) { subTask.BlockingReason = "not-docked"; return SubTaskOutcome.Failed; } var station = ResolveStation(world, ship.DockedStationId); if (station is null) { subTask.BlockingReason = "station-missing"; return SubTaskOutcome.Failed; } ship.TargetPosition = GetShipDockedPosition(ship, station); ship.Position = ship.TargetPosition; ship.State = ShipState.Loading; var itemId = subTask.ItemId; if (itemId is null) { return SubTaskOutcome.Completed; } var desiredAmount = subTask.Amount > 0f ? subTask.Amount : ship.Definition.GetTotalCargoCapacity(); var availableCapacity = MathF.Max(0f, ship.Definition.GetTotalCargoCapacity() - GetShipCargoAmount(ship)); var transferRate = balance.TransferRate * GetSkillFactor(ship.Skills.Trade); var moved = MathF.Min(transferRate * deltaSeconds, MathF.Min(availableCapacity, GetInventoryAmount(station.Inventory, itemId))); if (moved > 0.01f) { RemoveInventory(station.Inventory, itemId, moved); AddInventory(ship.Inventory, itemId, moved); } var loadedAmount = GetInventoryAmount(ship.Inventory, itemId); subTask.Progress = desiredAmount <= 0.01f ? 1f : Math.Clamp(loadedAmount / desiredAmount, 0f, 1f); return availableCapacity <= 0.01f || GetInventoryAmount(station.Inventory, itemId) <= 0.01f || loadedAmount >= desiredAmount - 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } private SubTaskOutcome UpdateUnloadCargoSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { if (ship.DockedStationId is null) { subTask.BlockingReason = "not-docked"; return SubTaskOutcome.Failed; } var station = ResolveStation(world, ship.DockedStationId); if (station is null) { subTask.BlockingReason = "station-missing"; return SubTaskOutcome.Failed; } ship.TargetPosition = GetShipDockedPosition(ship, station); ship.Position = ship.TargetPosition; ship.State = ShipState.Transferring; var transferRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Trade, ship.Skills.Mining)); if (subTask.ItemId is not null) { var moved = MathF.Min(transferRate * deltaSeconds, GetInventoryAmount(ship.Inventory, subTask.ItemId)); var accepted = TryAddStationInventory(world, station, subTask.ItemId, moved); RemoveInventory(ship.Inventory, subTask.ItemId, accepted); subTask.Progress = subTask.Amount <= 0.01f ? 1f : Math.Clamp(1f - (GetInventoryAmount(ship.Inventory, subTask.ItemId) / subTask.Amount), 0f, 1f); return GetInventoryAmount(ship.Inventory, subTask.ItemId) <= 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } foreach (var (itemId, amount) in ship.Inventory.ToList().OrderBy(entry => entry.Key, StringComparer.Ordinal)) { var moved = MathF.Min(amount, transferRate * deltaSeconds); var accepted = TryAddStationInventory(world, station, itemId, moved); RemoveInventory(ship.Inventory, itemId, accepted); if (accepted > 0.01f) { return GetShipCargoAmount(ship) <= 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } } return GetShipCargoAmount(ship) <= 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } private SubTaskOutcome UpdateTransferCargoToShipSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.Health > 0f); if (targetShip is null) { subTask.BlockingReason = "target-ship-missing"; return SubTaskOutcome.Failed; } var desiredPosition = GetFormationPosition(targetShip.Position, ship.Id, MathF.Max(subTask.Threshold, 12f)); subTask.TargetSystemId = targetShip.SystemId; subTask.TargetPosition = desiredPosition; if (ship.SystemId != targetShip.SystemId || ship.Position.DistanceTo(desiredPosition) > MathF.Max(subTask.Threshold, 12f)) { return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false); } ship.State = ShipState.Transferring; ship.TargetPosition = desiredPosition; ship.Position = ship.Position.MoveToward(desiredPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, ship.Position.DistanceTo(desiredPosition))); if (subTask.ItemId is null) { return SubTaskOutcome.Completed; } var targetCapacity = MathF.Max(0f, targetShip.Definition.GetTotalCargoCapacity() - GetShipCargoAmount(targetShip)); if (targetCapacity <= 0.01f) { subTask.BlockingReason = "target-cargo-full"; return SubTaskOutcome.Failed; } var transferRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Trade, ship.Skills.Navigation)); var desiredAmount = subTask.Amount > 0f ? subTask.Amount : GetInventoryAmount(ship.Inventory, subTask.ItemId); var moved = MathF.Min(transferRate * deltaSeconds, MathF.Min(targetCapacity, GetInventoryAmount(ship.Inventory, subTask.ItemId))); if (moved > 0.01f) { RemoveInventory(ship.Inventory, subTask.ItemId, moved); AddInventory(targetShip.Inventory, subTask.ItemId, moved); } var remaining = GetInventoryAmount(ship.Inventory, subTask.ItemId); subTask.Progress = desiredAmount <= 0.01f ? 1f : Math.Clamp(1f - (remaining / desiredAmount), 0f, 1f); return remaining <= 0.01f || GetShipCargoAmount(targetShip) >= targetShip.Definition.GetTotalCargoCapacity() - 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } private SubTaskOutcome UpdateSalvageSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { var wreck = world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.RemainingAmount > 0.01f); if (wreck is null) { return SubTaskOutcome.Completed; } var desiredPosition = subTask.TargetPosition ?? GetFormationPosition(wreck.Position, ship.Id, 8f); ship.TargetPosition = desiredPosition; if (ship.SystemId != wreck.SystemId || ship.Position.DistanceTo(desiredPosition) > MathF.Max(subTask.Threshold, 8f)) { subTask.TargetSystemId = wreck.SystemId; subTask.TargetPosition = desiredPosition; return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false); } ship.State = ShipState.Transferring; var remainingCapacity = MathF.Max(0f, ship.Definition.GetTotalCargoCapacity() - GetShipCargoAmount(ship)); if (remainingCapacity <= 0.01f) { return SubTaskOutcome.Completed; } if (!AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.4f, balance.MiningCycleSeconds * 0.8f))) { return SubTaskOutcome.Active; } var salvageRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Mining, ship.Skills.Trade)); var recovered = MathF.Min(salvageRate, MathF.Min(remainingCapacity, wreck.RemainingAmount)); if (recovered > 0.01f) { AddInventory(ship.Inventory, wreck.ItemId, recovered); wreck.RemainingAmount = MathF.Max(0f, wreck.RemainingAmount - recovered); } if (wreck.RemainingAmount <= 0.01f) { world.Wrecks.RemoveAll(candidate => candidate.Id == wreck.Id); } subTask.ElapsedSeconds = 0f; return wreck.RemainingAmount <= 0.01f || GetShipCargoAmount(ship) >= ship.Definition.GetTotalCargoCapacity() - 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } private SubTaskOutcome UpdateDeliverConstructionSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); var station = site is null ? null : ResolveSupportStation(world, ship, site); if (site is null || station is null || site.State is ConstructionSiteStateKinds.Completed or ConstructionSiteStateKinds.Destroyed) { subTask.BlockingReason = "construction-target-missing"; return SubTaskOutcome.Failed; } var supportPosition = ResolveSupportPosition(ship, station, site, world); if (!IsShipWithinSupportRange(ship, supportPosition, MathF.Max(12f, subTask.Threshold))) { ship.State = ShipState.LocalFlight; ship.TargetPosition = supportPosition; ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); return SubTaskOutcome.Active; } ship.TargetPosition = supportPosition; ship.Position = supportPosition; ship.State = ShipState.DeliveringConstruction; var transferRate = balance.TransferRate * GetSkillFactor(ship.Skills.Construction); foreach (var required in site.RequiredItems.OrderBy(entry => entry.Key, StringComparer.Ordinal)) { var delivered = GetInventoryAmount(site.DeliveredItems, required.Key); var remaining = MathF.Max(0f, required.Value - delivered); if (remaining <= 0.01f) { continue; } var available = MathF.Max(0f, GetInventoryAmount(station.Inventory, required.Key) - GetStationReserveFloor(world, station, required.Key)); var moved = MathF.Min(remaining, MathF.Min(available, transferRate * deltaSeconds)); if (moved <= 0.01f) { continue; } RemoveInventory(station.Inventory, required.Key, moved); AddInventory(site.Inventory, required.Key, moved); AddInventory(site.DeliveredItems, required.Key, moved); break; } subTask.Progress = site.RequiredItems.Count == 0 ? 1f : site.RequiredItems.Sum(required => required.Value <= 0.01f ? 1f : Math.Clamp(GetInventoryAmount(site.DeliveredItems, required.Key) / required.Value, 0f, 1f)) / site.RequiredItems.Count; return IsConstructionSiteReady(world, site) ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } private SubTaskOutcome UpdateBuildConstructionSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) { var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); var station = site is null ? null : ResolveSupportStation(world, ship, site); if (site is null || station is null || site.BlueprintId is null || site.State is ConstructionSiteStateKinds.Completed or ConstructionSiteStateKinds.Destroyed) { subTask.BlockingReason = "construction-site-missing"; return SubTaskOutcome.Failed; } var supportPosition = ResolveSupportPosition(ship, station, site, world); if (!IsShipWithinSupportRange(ship, supportPosition, MathF.Max(12f, subTask.Threshold))) { ship.State = ShipState.LocalFlight; ship.TargetPosition = supportPosition; ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); return SubTaskOutcome.Active; } if (!IsConstructionSiteReady(world, site) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe)) { ship.State = ShipState.WaitingMaterials; subTask.Status = WorkStatus.Blocked; subTask.BlockingReason = "waiting-materials"; return SubTaskOutcome.Active; } subTask.Status = WorkStatus.Active; subTask.BlockingReason = null; ship.TargetPosition = supportPosition; ship.Position = supportPosition; ship.State = ShipState.Constructing; site.AssignedConstructorShipIds.Add(ship.Id); site.Progress += deltaSeconds * GetSkillFactor(ship.Skills.Construction); subTask.Progress = recipe.Duration <= 0.01f ? 1f : Math.Clamp(site.Progress / recipe.Duration, 0f, 1f); if (site.Progress < recipe.Duration) { return SubTaskOutcome.Active; } if (site.StationId is null) { CompleteStationFoundation(world, station, site); } else { AddStationModule(world, station, site.BlueprintId); PrepareNextConstructionSiteStep(world, station, site); } site.State = ConstructionSiteStateKinds.Completed; return SubTaskOutcome.Completed; } private static bool AdvanceTimedSubTask(ShipSubTaskRuntime subTask, float deltaSeconds, float requiredSeconds) { subTask.TotalSeconds = requiredSeconds; subTask.ElapsedSeconds += deltaSeconds; subTask.Progress = requiredSeconds <= 0.01f ? 1f : Math.Clamp(subTask.ElapsedSeconds / requiredSeconds, 0f, 1f); if (subTask.ElapsedSeconds < requiredSeconds) { return false; } subTask.ElapsedSeconds = 0f; return true; } private SubTaskOutcome UpdateLocalTravel( SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds, string targetSystemId, Vector3 targetPosition, AnchorRuntime? currentAnchor, AnchorRuntime? targetAnchor, bool completeOnArrival) { var distance = ship.Position.DistanceTo(targetPosition); ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; ship.SpatialState.Transit = null; ship.SpatialState.DestinationAnchorId = targetAnchor?.Id ?? currentAnchor?.Id; subTask.Progress = Math.Clamp(1f - (distance / MathF.Max(distance + GetLocalTravelSpeed(ship), 1f)), 0f, 1f); var localSystemOffset = SimulationUnits.MetersToKilometers(ship.Position); ship.SpatialState.SystemPosition = currentAnchor is null ? localSystemOffset : new Vector3( currentAnchor.Position.X + localSystemOffset.X, currentAnchor.Position.Y + localSystemOffset.Y, currentAnchor.Position.Z + localSystemOffset.Z); if (distance <= MathF.Max(subTask.Threshold, balance.ArrivalThreshold)) { ship.Position = targetPosition; ship.TargetPosition = targetPosition; ship.SystemId = targetSystemId; ship.SpatialState.CurrentSystemId = targetSystemId; ship.SpatialState.CurrentAnchorId = targetAnchor?.Id ?? currentAnchor?.Id; var arrivalSystemOffset = SimulationUnits.MetersToKilometers(targetPosition); ship.SpatialState.SystemPosition = targetAnchor is null ? arrivalSystemOffset : new Vector3( targetAnchor.Position.X + arrivalSystemOffset.X, targetAnchor.Position.Y + arrivalSystemOffset.Y, targetAnchor.Position.Z + arrivalSystemOffset.Z); ship.State = ShipState.Arriving; return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } ship.State = ShipState.LocalFlight; ship.SpatialState.CurrentAnchorId = currentAnchor?.Id; ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); var movedSystemOffset = SimulationUnits.MetersToKilometers(ship.Position); ship.SpatialState.SystemPosition = currentAnchor is null ? movedSystemOffset : new Vector3( currentAnchor.Position.X + movedSystemOffset.X, currentAnchor.Position.Y + movedSystemOffset.Y, currentAnchor.Position.Z + movedSystemOffset.Z); return SubTaskOutcome.Active; } private SubTaskOutcome UpdateWarpTransit( SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds, Vector3 targetPosition, AnchorRuntime currentAnchor, AnchorRuntime targetAnchor, bool completeOnArrival) { var transit = ship.SpatialState.Transit; if (transit is null || transit.Regime != MovementRegimeKind.Warp || transit.DestinationAnchorId != targetAnchor.Id) { var originAnchorPosition = currentAnchor.Position; var destinationAnchorPosition = targetAnchor.Position; var initialSpoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f); var initialTravelDuration = MathF.Max(0.1f, originAnchorPosition.DistanceTo(destinationAnchorPosition) / MathF.Max(GetWarpTravelSpeed(ship), 0.001f)); transit = new ShipTransitRuntime { Regime = MovementRegimeKind.Warp, OriginAnchorId = currentAnchor.Id, DestinationAnchorId = targetAnchor.Id, StartedAtUtc = world.GeneratedAtUtc, ArrivalDueAtUtc = world.GeneratedAtUtc.AddSeconds(initialSpoolDuration + initialTravelDuration), }; ship.SpatialState.Transit = transit; subTask.ElapsedSeconds = 0f; } ship.SpatialState.SpaceLayer = SpaceLayerKind.SystemSpace; ship.SpatialState.MovementRegime = MovementRegimeKind.Warp; ship.SpatialState.CurrentAnchorId = null; ship.SpatialState.DestinationAnchorId = targetAnchor.Id; var spoolDurationSeconds = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f); var startedAtUtc = transit.StartedAtUtc ?? world.GeneratedAtUtc; var arrivalDueAtUtc = transit.ArrivalDueAtUtc ?? world.GeneratedAtUtc; var totalDuration = MathF.Max(0.1f, (float)(arrivalDueAtUtc - startedAtUtc).TotalSeconds); var elapsedSeconds = Math.Clamp((float)(world.GeneratedAtUtc - startedAtUtc).TotalSeconds, 0f, totalDuration); var originPosition = ResolveAnchorPosition(world, transit.OriginAnchorId, currentAnchor.Position); var destinationPosition = ResolveAnchorPosition(world, transit.DestinationAnchorId, targetAnchor.Position); if (elapsedSeconds < spoolDurationSeconds) { ship.State = ShipState.SpoolingWarp; ship.Position = Vector3.Zero; ship.TargetPosition = Vector3.Zero; ship.SpatialState.SystemPosition = originPosition; transit.Progress = Math.Clamp(elapsedSeconds / totalDuration, 0f, 1f); subTask.Progress = transit.Progress; return SubTaskOutcome.Active; } ship.State = ShipState.Warping; var warpTravelDuration = MathF.Max(0.001f, totalDuration - spoolDurationSeconds); var travelElapsed = Math.Clamp(elapsedSeconds - spoolDurationSeconds, 0f, warpTravelDuration); var travelProgress = Math.Clamp(travelElapsed / warpTravelDuration, 0f, 1f); var travelDelta = destinationPosition.Subtract(originPosition); ship.Position = Vector3.Zero; ship.TargetPosition = Vector3.Zero; ship.SpatialState.SystemPosition = new Vector3( originPosition.X + (travelDelta.X * travelProgress), originPosition.Y + (travelDelta.Y * travelProgress), originPosition.Z + (travelDelta.Z * travelProgress)); transit.Progress = Math.Clamp(elapsedSeconds / totalDuration, 0f, 1f); subTask.Progress = transit.Progress; if (elapsedSeconds < totalDuration - 0.001f) { return SubTaskOutcome.Active; } return CompleteTransitArrival(ship, subTask.TargetSystemId ?? ship.SystemId, targetPosition, targetAnchor, completeOnArrival); } private SubTaskOutcome UpdateFtlTransit( SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds, string targetSystemId, Vector3 entryPosition, AnchorRuntime? entryAnchor, bool completeOnArrival, Vector3 finalTargetPosition, AnchorRuntime? finalTargetAnchor) { var destinationAnchorId = entryAnchor?.Id; var transit = ship.SpatialState.Transit; if (transit is null || transit.Regime != MovementRegimeKind.FtlTransit || transit.DestinationAnchorId != destinationAnchorId) { var initialTravelDuration = MathF.Max(0.1f, ResolveSystemGalaxyPosition(world, ship.SystemId).DistanceTo(ResolveSystemGalaxyPosition(world, targetSystemId)) / MathF.Max(ship.Definition.FtlSpeed * GetSkillFactor(ship.Skills.Navigation), 0.001f)); var initialSpoolDuration = MathF.Max(ship.Definition.SpoolTime, 0.1f); transit = new ShipTransitRuntime { Regime = MovementRegimeKind.FtlTransit, OriginAnchorId = ship.SpatialState.CurrentAnchorId, DestinationAnchorId = destinationAnchorId, StartedAtUtc = world.GeneratedAtUtc, ArrivalDueAtUtc = world.GeneratedAtUtc.AddSeconds(initialSpoolDuration + initialTravelDuration), }; ship.SpatialState.Transit = transit; subTask.ElapsedSeconds = 0f; } ship.SpatialState.SpaceLayer = SpaceLayerKind.GalaxySpace; ship.SpatialState.MovementRegime = MovementRegimeKind.FtlTransit; ship.SpatialState.CurrentAnchorId = null; ship.SpatialState.DestinationAnchorId = destinationAnchorId; var spoolDurationSeconds = MathF.Max(ship.Definition.SpoolTime, 0.1f); var startedAtUtc = transit.StartedAtUtc ?? world.GeneratedAtUtc; var arrivalDueAtUtc = transit.ArrivalDueAtUtc ?? world.GeneratedAtUtc; var totalDuration = MathF.Max(0.1f, (float)(arrivalDueAtUtc - startedAtUtc).TotalSeconds); var elapsedSeconds = Math.Clamp((float)(world.GeneratedAtUtc - startedAtUtc).TotalSeconds, 0f, totalDuration); ship.State = elapsedSeconds < spoolDurationSeconds ? ShipState.SpoolingFtl : ShipState.Ftl; transit.Progress = Math.Clamp(elapsedSeconds / totalDuration, 0f, 1f); subTask.Progress = transit.Progress; if (elapsedSeconds < totalDuration - 0.001f) { return SubTaskOutcome.Active; } ship.Position = Vector3.Zero; ship.TargetPosition = finalTargetPosition; ship.SystemId = targetSystemId; ship.SpatialState.CurrentSystemId = targetSystemId; ship.SpatialState.Transit = null; ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; ship.SpatialState.CurrentAnchorId = entryAnchor?.Id; ship.SpatialState.DestinationAnchorId = finalTargetAnchor?.Id ?? entryAnchor?.Id; ship.SpatialState.SystemPosition = entryPosition; ship.State = ShipState.Arriving; // Cross-system travel is only complete once the ship finishes the // destination-system local leg to the actual target. return SubTaskOutcome.Active; } private static SubTaskOutcome CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, AnchorRuntime? targetAnchor, bool completeOnArrival) { ship.Position = targetPosition; ship.TargetPosition = targetPosition; ship.SystemId = targetSystemId; ship.SpatialState.CurrentSystemId = targetSystemId; ship.SpatialState.Transit = null; ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; ship.SpatialState.CurrentAnchorId = targetAnchor?.Id; ship.SpatialState.DestinationAnchorId = targetAnchor?.Id; var arrivalSystemOffset = SimulationUnits.MetersToKilometers(targetPosition); ship.SpatialState.SystemPosition = targetAnchor is null ? arrivalSystemOffset : new Vector3( targetAnchor.Position.X + arrivalSystemOffset.X, targetAnchor.Position.Y + arrivalSystemOffset.Y, targetAnchor.Position.Z + arrivalSystemOffset.Z); ship.State = ShipState.Arriving; return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active; } }