Files
space-game/apps/backend/Ships/Runtime/ShipRuntimeModels.cs

314 lines
11 KiB
C#

namespace SpaceGame.Api.Ships.Runtime;
public sealed class ShipRuntime
{
public required string Id { get; init; }
public required string SystemId { get; set; }
public required ShipDefinition Definition { get; init; }
public required string FactionId { get; init; }
public required Vector3 Position { get; set; }
public required Vector3 TargetPosition { get; set; }
public required ShipSpatialStateRuntime SpatialState { get; set; }
public Vector3 Velocity { get; set; } = Vector3.Zero;
public ShipState State { get; set; } = ShipState.Idle;
public required DefaultBehaviorRuntime DefaultBehavior { get; set; }
public ShipOrderQueue OrderQueue { get; } = new();
public required ShipSkillProfileRuntime Skills { get; set; }
public bool NeedsReplan { get; set; } = true;
public float ReplanCooldownSeconds { get; set; }
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
public string? DockedStationId { get; set; }
public int? AssignedDockingPadIndex { get; set; }
public string? CommanderId { get; set; }
public string? PolicySetId { get; set; }
public string ControlSourceKind { get; set; } = "unassigned";
public string? ControlSourceId { get; set; }
public string? ControlReason { get; set; }
public string? LastReplanReason { get; set; }
public string? LastAccessFailureReason { get; set; }
public float Health { get; set; }
public HashSet<string> KnownStationIds { get; } = new(StringComparer.Ordinal);
public List<string> History { get; } = [];
public string? ActiveOrderId { get; set; }
public int ActiveSubTaskIndex { get; set; }
public List<ShipSubTaskRuntime> ActiveSubTasks { get; } = [];
public string LastSignature { get; set; } = string.Empty;
public string LastDeltaSignature { get; set; } = string.Empty;
}
public sealed class ShipOrderQueue : IReadOnlyList<ShipOrderRuntime>
{
public const int MaxOrders = 8;
private readonly List<ShipOrderRuntime> _orders = [];
public int Count => _orders.Count;
public ShipOrderRuntime this[int index] => _orders[index];
public IEnumerator<ShipOrderRuntime> GetEnumerator() => _orders.GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
public void Enqueue(ShipOrderRuntime order)
{
if (_orders.Count >= MaxOrders)
{
throw new InvalidOperationException("Order queue is full.");
}
_orders.Add(order);
}
public void EnqueuePlayerOrder(ShipOrderRuntime order)
{
if (order.SourceKind != ShipOrderSourceKind.Player)
{
throw new InvalidOperationException("Player segment only accepts player orders.");
}
EnsureCapacityForNewOrder(order.Id);
_orders.Insert(GetManagedInsertionIndex(), order);
}
public void EnqueueManagedOrder(ShipOrderRuntime order)
{
EnsureCapacityForNewOrder(order.Id);
_orders.Add(order);
}
public void AddOrReplaceManagedOrder(ShipOrderRuntime order)
=> AddOrReplaceManagedOrder(order, insertAtFront: false);
public void AddOrReplaceManagedOrderAtFront(ShipOrderRuntime order)
=> AddOrReplaceManagedOrder(order, insertAtFront: true);
private void AddOrReplaceManagedOrder(ShipOrderRuntime order, bool insertAtFront)
{
var existingIndex = _orders.FindIndex(candidate => string.Equals(candidate.Id, order.Id, StringComparison.Ordinal));
if (existingIndex >= 0)
{
_orders[existingIndex] = order;
return;
}
EnsureCapacityForNewOrder(order.Id);
if (insertAtFront)
{
_orders.Insert(GetManagedInsertionIndex(), order);
return;
}
_orders.Add(order);
}
public bool Remove(ShipOrderRuntime order) => RemoveById(order.Id);
public bool RemoveById(string orderId) => _orders.RemoveAll(order => string.Equals(order.Id, orderId, StringComparison.Ordinal)) > 0;
public int RemoveWhere(Predicate<ShipOrderRuntime> predicate) => _orders.RemoveAll(predicate);
public ShipOrderRuntime? FindById(string orderId) => _orders.FirstOrDefault(order => string.Equals(order.Id, orderId, StringComparison.Ordinal));
public ShipOrderRuntime? FindLeadingOrderForSource(ShipOrderSourceKind sourceKind) =>
_orders.FirstOrDefault(order => order.SourceKind == sourceKind);
public string? GetLeadingOrderLabelForSource(ShipOrderSourceKind sourceKind) =>
FindLeadingOrderForSource(sourceKind) is { } order
? order.Label ?? order.Kind
: null;
public bool HasOrdersFromSource(ShipOrderSourceKind sourceKind) => _orders.Any(order => order.SourceKind == sourceKind);
public ShipOrderRuntime? GetCurrentOrder() =>
_orders.FirstOrDefault(order => order.Status is OrderStatus.Queued or OrderStatus.Active);
public bool TryMovePlayerOrder(string orderId, int targetIndex)
{
var currentIndex = _orders.FindIndex(order => string.Equals(order.Id, orderId, StringComparison.Ordinal));
if (currentIndex < 0)
{
return false;
}
var order = _orders[currentIndex];
if (order.SourceKind != ShipOrderSourceKind.Player)
{
return false;
}
var playerOrderIds = _orders
.Select((candidate, index) => (candidate, index))
.Where(entry => entry.candidate.SourceKind == ShipOrderSourceKind.Player)
.Select(entry => entry.index)
.ToList();
if (playerOrderIds.Count <= 1)
{
return true;
}
var clampedPlayerIndex = Math.Clamp(targetIndex, 0, playerOrderIds.Count - 1);
var destinationIndex = playerOrderIds[clampedPlayerIndex];
if (currentIndex == destinationIndex)
{
return true;
}
_orders.RemoveAt(currentIndex);
if (currentIndex < destinationIndex)
{
destinationIndex -= 1;
}
_orders.Insert(destinationIndex, order);
return true;
}
public bool TryCompleteOrder(string orderId) => TryTransitionOrder(orderId, OrderStatus.Completed);
public bool TryFailOrder(string orderId, string? failureReason = null)
{
var order = FindById(orderId);
if (order is null)
{
return false;
}
order.FailureReason = failureReason ?? order.FailureReason;
if (order.SourceKind == ShipOrderSourceKind.Player)
{
order.Status = OrderStatus.Failed;
return true;
}
return TryTransitionOrder(orderId, OrderStatus.Failed);
}
public bool TryTransitionOrder(string orderId, OrderStatus terminalStatus)
{
var order = FindById(orderId);
if (order is null)
{
return false;
}
order.Status = terminalStatus;
return RemoveById(orderId);
}
private int GetManagedInsertionIndex() =>
_orders.TakeWhile(order => order.SourceKind == ShipOrderSourceKind.Player).Count();
private void EnsureCapacityForNewOrder(string orderId)
{
if (FindById(orderId) is not null)
{
return;
}
if (_orders.Count >= MaxOrders)
{
throw new InvalidOperationException("Order queue is full.");
}
}
}
public sealed class ShipSkillProfileRuntime
{
public int Navigation { get; set; }
public int Trade { get; set; }
public int Mining { get; set; }
public int Combat { get; set; }
public int Construction { get; set; }
}
public sealed class ShipOrderRuntime
{
public required string Id { get; init; }
public required string Kind { get; init; }
public required ShipOrderSourceKind SourceKind { get; init; }
public required string SourceId { get; init; }
public OrderStatus Status { get; set; } = OrderStatus.Queued;
public int Priority { get; set; }
public bool InterruptCurrentPlan { get; set; } = true;
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
public string? Label { get; set; }
public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; }
public Vector3? TargetPosition { get; set; }
public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; }
public string? ItemId { get; set; }
public string? AnchorId { get; set; }
public string? ConstructionSiteId { get; set; }
public string? ModuleId { get; set; }
public float WaitSeconds { get; set; }
public float Radius { get; set; }
public int? MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
public string? FailureReason { get; set; }
}
public sealed class DefaultBehaviorRuntime
{
public required string Kind { get; set; }
public string? HomeSystemId { get; set; }
public string? HomeStationId { get; set; }
public string? AreaSystemId { get; set; }
public string? TargetEntityId { get; set; }
public string? ItemId { get; set; }
public string? PreferredAnchorId { get; set; }
public string? PreferredConstructionSiteId { get; set; }
public string? PreferredModuleId { get; set; }
public Vector3? TargetPosition { get; set; }
public float WaitSeconds { get; set; } = 3f;
public float Radius { get; set; } = 24f;
public int MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
public List<Vector3> PatrolPoints { get; set; } = [];
public int PatrolIndex { get; set; }
public List<ShipOrderTemplateRuntime> RepeatOrders { get; set; } = [];
public int RepeatIndex { get; set; }
}
public sealed class ShipOrderTemplateRuntime
{
public required string Kind { get; init; }
public string? Label { get; set; }
public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; }
public Vector3? TargetPosition { get; set; }
public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; }
public string? ItemId { get; set; }
public string? AnchorId { get; set; }
public string? ConstructionSiteId { get; set; }
public string? ModuleId { get; set; }
public float WaitSeconds { get; set; }
public float Radius { get; set; }
public int? MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
}
public sealed class ShipSubTaskRuntime
{
public required string Id { get; init; }
public required string Kind { get; init; }
public required string Summary { get; set; }
public WorkStatus Status { get; set; } = WorkStatus.Pending;
public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; }
public string? TargetAnchorId { get; set; }
public string? TargetResourceNodeId { get; set; }
public string? TargetResourceDepositId { get; set; }
public Vector3? TargetPosition { get; set; }
public string? ItemId { get; set; }
public string? ModuleId { get; set; }
public float Threshold { get; set; }
public float Amount { get; set; }
public float ElapsedSeconds { get; set; }
public float TotalSeconds { get; set; }
public float Progress { get; set; }
public string? BlockingReason { get; set; }
}