771 lines
36 KiB
C#
771 lines
36 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, ShipPlanStepRuntime step, 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 targetCelestial = ResolveTravelTargetCelestial(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 destinationEntryCelestial = ResolveSystemEntryCelestial(world, subTask.TargetSystemId);
|
|
var destinationEntryPosition = destinationEntryCelestial?.Position ?? targetPosition;
|
|
return UpdateFtlTransit(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, destinationEntryPosition, destinationEntryCelestial, completeOnArrival, targetPosition);
|
|
}
|
|
|
|
var currentCelestial = ResolveCurrentCelestial(world, ship);
|
|
if (targetCelestial is not null
|
|
&& currentCelestial is not null
|
|
&& !string.Equals(currentCelestial.Id, targetCelestial.Id, StringComparison.Ordinal))
|
|
{
|
|
if (!CanWarp(ship.Definition))
|
|
{
|
|
return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, targetCelestial, completeOnArrival);
|
|
}
|
|
|
|
return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, targetCelestial, completeOnArrival);
|
|
}
|
|
|
|
if (targetCelestial is not null
|
|
&& ship.Position.DistanceTo(targetPosition) > WarpEngageDistanceKilometers
|
|
&& CanWarp(ship.Definition))
|
|
{
|
|
return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, targetCelestial, completeOnArrival);
|
|
}
|
|
|
|
return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, targetCelestial, 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.TargetEntityId ?? subTask.TargetNodeId);
|
|
if (node is null || !CanExtractNode(ship, node, world))
|
|
{
|
|
subTask.BlockingReason = "node-missing";
|
|
ship.State = ShipState.Blocked;
|
|
return SubTaskOutcome.Failed;
|
|
}
|
|
|
|
var targetPosition = subTask.TargetPosition ?? GetResourceHoldPosition(node.Position, ship.Id, 20f);
|
|
ship.TargetPosition = targetPosition;
|
|
if (ship.Position.DistanceTo(targetPosition) > MathF.Max(subTask.Threshold, 8f))
|
|
{
|
|
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, node.OreRemaining);
|
|
if (mined <= 0.01f)
|
|
{
|
|
return SubTaskOutcome.Completed;
|
|
}
|
|
|
|
AddInventory(ship.Inventory, node.ItemId, mined);
|
|
node.OreRemaining = MathF.Max(0f, node.OreRemaining - mined);
|
|
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,
|
|
CelestialRuntime? targetCelestial,
|
|
bool completeOnArrival)
|
|
{
|
|
var distance = ship.Position.DistanceTo(targetPosition);
|
|
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
|
|
ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
|
|
ship.SpatialState.Transit = null;
|
|
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
|
|
subTask.Progress = Math.Clamp(1f - (distance / MathF.Max(distance + GetLocalTravelSpeed(ship), 1f)), 0f, 1f);
|
|
|
|
if (distance <= MathF.Max(subTask.Threshold, balance.ArrivalThreshold))
|
|
{
|
|
ship.Position = targetPosition;
|
|
ship.TargetPosition = targetPosition;
|
|
ship.SystemId = targetSystemId;
|
|
ship.SpatialState.CurrentSystemId = targetSystemId;
|
|
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
|
|
ship.State = ShipState.Arriving;
|
|
return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
|
|
}
|
|
|
|
ship.State = ShipState.LocalFlight;
|
|
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
|
return SubTaskOutcome.Active;
|
|
}
|
|
|
|
private SubTaskOutcome UpdateWarpTransit(
|
|
SimulationWorld world,
|
|
ShipRuntime ship,
|
|
ShipSubTaskRuntime subTask,
|
|
float deltaSeconds,
|
|
Vector3 targetPosition,
|
|
CelestialRuntime targetCelestial,
|
|
bool completeOnArrival)
|
|
{
|
|
var transit = ship.SpatialState.Transit;
|
|
if (transit is null || transit.Regime != MovementRegimeKind.Warp || transit.DestinationNodeId != targetCelestial.Id)
|
|
{
|
|
transit = new ShipTransitRuntime
|
|
{
|
|
Regime = MovementRegimeKind.Warp,
|
|
OriginNodeId = ship.SpatialState.CurrentCelestialId,
|
|
DestinationNodeId = targetCelestial.Id,
|
|
StartedAtUtc = world.GeneratedAtUtc,
|
|
};
|
|
ship.SpatialState.Transit = transit;
|
|
subTask.ElapsedSeconds = 0f;
|
|
}
|
|
|
|
ship.SpatialState.SpaceLayer = SpaceLayerKind.SystemSpace;
|
|
ship.SpatialState.MovementRegime = MovementRegimeKind.Warp;
|
|
ship.SpatialState.CurrentCelestialId = null;
|
|
ship.SpatialState.DestinationNodeId = targetCelestial.Id;
|
|
|
|
var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
|
|
if (ship.State != ShipState.Warping)
|
|
{
|
|
ship.State = ShipState.SpoolingWarp;
|
|
if (!AdvanceTimedSubTask(subTask, deltaSeconds, spoolDuration))
|
|
{
|
|
return SubTaskOutcome.Active;
|
|
}
|
|
|
|
ship.State = ShipState.Warping;
|
|
}
|
|
|
|
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
|
|
? ship.Position.DistanceTo(targetPosition)
|
|
: (world.Celestials.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
|
|
ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds);
|
|
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
|
|
subTask.Progress = transit.Progress;
|
|
if (ship.Position.DistanceTo(targetPosition) > 18f)
|
|
{
|
|
return SubTaskOutcome.Active;
|
|
}
|
|
|
|
return CompleteTransitArrival(ship, subTask.TargetSystemId ?? ship.SystemId, targetPosition, targetCelestial, completeOnArrival);
|
|
}
|
|
|
|
private SubTaskOutcome UpdateFtlTransit(
|
|
SimulationWorld world,
|
|
ShipRuntime ship,
|
|
ShipSubTaskRuntime subTask,
|
|
float deltaSeconds,
|
|
string targetSystemId,
|
|
Vector3 entryPosition,
|
|
CelestialRuntime? targetCelestial,
|
|
bool completeOnArrival,
|
|
Vector3 finalTargetPosition)
|
|
{
|
|
var destinationNodeId = targetCelestial?.Id;
|
|
var transit = ship.SpatialState.Transit;
|
|
if (transit is null || transit.Regime != MovementRegimeKind.FtlTransit || transit.DestinationNodeId != destinationNodeId)
|
|
{
|
|
transit = new ShipTransitRuntime
|
|
{
|
|
Regime = MovementRegimeKind.FtlTransit,
|
|
OriginNodeId = ship.SpatialState.CurrentCelestialId,
|
|
DestinationNodeId = destinationNodeId,
|
|
StartedAtUtc = world.GeneratedAtUtc,
|
|
};
|
|
ship.SpatialState.Transit = transit;
|
|
subTask.ElapsedSeconds = 0f;
|
|
}
|
|
|
|
ship.SpatialState.SpaceLayer = SpaceLayerKind.GalaxySpace;
|
|
ship.SpatialState.MovementRegime = MovementRegimeKind.FtlTransit;
|
|
ship.SpatialState.CurrentCelestialId = null;
|
|
ship.SpatialState.DestinationNodeId = destinationNodeId;
|
|
|
|
if (ship.State != ShipState.Ftl)
|
|
{
|
|
ship.State = ShipState.SpoolingFtl;
|
|
if (!AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(ship.Definition.SpoolTime, 0.1f)))
|
|
{
|
|
return SubTaskOutcome.Active;
|
|
}
|
|
|
|
ship.State = ShipState.Ftl;
|
|
}
|
|
|
|
var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
|
|
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
|
|
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));
|
|
transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * GetSkillFactor(ship.Skills.Navigation)) * deltaSeconds / totalDistance));
|
|
subTask.Progress = transit.Progress;
|
|
if (transit.Progress < 0.999f)
|
|
{
|
|
return SubTaskOutcome.Active;
|
|
}
|
|
|
|
ship.Position = entryPosition;
|
|
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.CurrentCelestialId = targetCelestial?.Id;
|
|
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
|
|
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, CelestialRuntime? targetCelestial, 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.CurrentCelestialId = targetCelestial?.Id;
|
|
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
|
|
ship.State = ShipState.Arriving;
|
|
return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
|
|
}
|
|
}
|