Files
space-game/apps/backend/Ships/AI/ShipAiService.Execution.cs

839 lines
40 KiB
C#

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;
}
}