Files
space-game/apps/backend/Simulation/SimulationEngine.ShipActionSystem.cs

521 lines
18 KiB
C#

namespace SpaceGame.Simulation.Api.Simulation;
public sealed partial class SimulationEngine
{
private static bool AdvanceTimedAction(ShipRuntime ship, float deltaSeconds, float requiredSeconds)
{
ship.ActionTimer += deltaSeconds;
if (ship.ActionTimer < requiredSeconds)
{
return false;
}
ship.ActionTimer = 0f;
return true;
}
private static void BeginTrackedAction(ShipRuntime ship, string actionKey, float total)
{
if (ship.TrackedActionKey == actionKey)
{
return;
}
ship.TrackedActionKey = actionKey;
ship.TrackedActionTotal = MathF.Max(total, 0.01f);
}
internal static float GetShipCargoAmount(ShipRuntime ship)
{
var cargoItemId = ship.Definition.CargoItemId;
return cargoItemId is null ? 0f : GetInventoryAmount(ship.Inventory, cargoItemId);
}
private string UpdateExtract(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
var task = ship.ControllerTask;
var node = world.Nodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (node is null || task.TargetPosition is null || !CanExtractNode(ship, node))
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
var cargoAmount = GetShipCargoAmount(ship);
if (cargoAmount >= ship.Definition.CargoCapacity - 0.01f)
{
ship.ActionTimer = 0f;
ship.State = ShipState.CargoFull;
ship.TargetPosition = ship.Position;
return "cargo-full";
}
ship.TargetPosition = task.TargetPosition.Value;
var distance = ship.Position.DistanceTo(task.TargetPosition.Value);
if (distance > task.Threshold)
{
ship.ActionTimer = 0f;
ship.State = ShipState.MiningApproach;
ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, GetLocalTravelSpeed(ship) * deltaSeconds);
return "none";
}
ship.State = ShipState.Mining;
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.MiningCycleSeconds))
{
return "none";
}
var remainingCapacity = MathF.Max(0f, ship.Definition.CargoCapacity - cargoAmount);
var mined = MathF.Min(world.Balance.MiningRate, remainingCapacity);
mined = MathF.Min(mined, node.OreRemaining);
if (mined <= 0.01f)
{
ship.ActionTimer = 0f;
ship.State = node.OreRemaining <= 0.01f ? ShipState.NodeDepleted : ShipState.CargoFull;
ship.TargetPosition = ship.Position;
return node.OreRemaining <= 0.01f ? "node-depleted" : "cargo-full";
}
if (ship.Definition.CargoItemId is not null)
{
AddInventory(ship.Inventory, ship.Definition.CargoItemId, mined);
}
node.OreRemaining -= mined;
node.OreRemaining = MathF.Max(0f, node.OreRemaining);
return GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "cargo-full" : "none";
}
private string UpdateDock(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
var task = ship.ControllerTask;
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (station is null || task.TargetPosition is null)
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
var padIndex = ship.AssignedDockingPadIndex ?? ReserveDockingPad(station, ship.Id);
if (padIndex is null)
{
ship.ActionTimer = 0f;
ship.State = ShipState.AwaitingDock;
ship.TargetPosition = GetDockingHoldPosition(station, ship.Id);
var waitDistance = ship.Position.DistanceTo(ship.TargetPosition);
if (waitDistance > 4f)
{
ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
}
return "none";
}
ship.AssignedDockingPadIndex = padIndex;
var padPosition = GetDockingPadPosition(station, padIndex.Value);
ship.TargetPosition = padPosition;
var distance = ship.Position.DistanceTo(padPosition);
if (distance > 4f)
{
ship.ActionTimer = 0f;
ship.State = ShipState.DockingApproach;
ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
return "none";
}
ship.State = ShipState.Docking;
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.DockingDuration))
{
return "none";
}
ship.State = ShipState.Docked;
ship.DockedStationId = station.Id;
station.DockedShipIds.Add(ship.Id);
ship.Position = padPosition;
ship.TargetPosition = padPosition;
return "docked";
}
private string UpdateUnload(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
if (ship.DockedStationId is null)
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
if (station is null)
{
ship.DockedStationId = null;
ship.AssignedDockingPadIndex = null;
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
ship.TargetPosition = GetShipDockedPosition(ship, station);
ship.Position = ship.TargetPosition;
ship.ActionTimer = 0f;
ship.State = ShipState.Transferring;
BeginTrackedAction(ship, "transferring", GetShipCargoAmount(ship));
var cargoItemId = ship.Definition.CargoItemId;
var moved = cargoItemId is null ? 0f : MathF.Min(GetInventoryAmount(ship.Inventory, cargoItemId), world.Balance.TransferRate * deltaSeconds);
if (cargoItemId is not null)
{
var accepted = TryAddStationInventory(world, station, cargoItemId, moved);
RemoveInventory(ship.Inventory, cargoItemId, accepted);
moved = accepted;
}
var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId);
if (faction is not null && cargoItemId == "ore")
{
faction.OreMined += moved;
faction.Credits += moved * 0.4f;
}
return cargoItemId is null || GetInventoryAmount(ship.Inventory, cargoItemId) <= 0.01f ? "unloaded" : "none";
}
private string UpdateLoadCargo(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
if (ship.DockedStationId is null)
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
if (station is null)
{
ship.DockedStationId = null;
ship.AssignedDockingPadIndex = null;
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
ship.TargetPosition = GetShipDockedPosition(ship, station);
ship.Position = ship.TargetPosition;
ship.ActionTimer = 0f;
ship.State = ShipState.Loading;
BeginTrackedAction(ship, "loading", MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship)));
var cargoItemId = ship.Definition.CargoItemId;
var transfer = MathF.Min(world.Balance.TransferRate * deltaSeconds, ship.Definition.CargoCapacity - GetShipCargoAmount(ship));
var moved = cargoItemId is null ? 0f : MathF.Min(transfer, GetInventoryAmount(station.Inventory, cargoItemId));
if (cargoItemId is not null && moved > 0.01f)
{
RemoveInventory(station.Inventory, cargoItemId, moved);
AddInventory(ship.Inventory, cargoItemId, moved);
}
return cargoItemId is null
|| GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f
|| GetInventoryAmount(station.Inventory, cargoItemId) <= 0.01f
? "loaded"
: "none";
}
private string UpdateConstructModule(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
var station = ResolveShipSupportStation(ship, world);
if (station is null || ship.DefaultBehavior.ModuleId is null)
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
if (!world.ModuleRecipes.TryGetValue(ship.DefaultBehavior.ModuleId, out var recipe))
{
ship.AssignedDockingPadIndex = null;
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
var supportPosition = ResolveShipSupportPosition(ship, station);
if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold))
{
ship.State = ShipState.LocalFlight;
ship.TargetPosition = supportPosition;
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
return "none";
}
if (!TryEnsureModuleConstructionStarted(station, recipe, ship.Id))
{
ship.ActionTimer = 0f;
ship.State = ShipState.WaitingMaterials;
ship.TargetPosition = supportPosition;
return "none";
}
if (station.ActiveConstruction?.AssignedConstructorShipId != ship.Id)
{
ship.State = ShipState.ConstructionBlocked;
ship.TargetPosition = supportPosition;
return "none";
}
ship.TargetPosition = supportPosition;
ship.Position = ship.TargetPosition;
ship.ActionTimer = 0f;
ship.State = ShipState.Constructing;
station.ActiveConstruction.ProgressSeconds += deltaSeconds;
if (station.ActiveConstruction.ProgressSeconds < station.ActiveConstruction.RequiredSeconds)
{
return "none";
}
AddStationModule(world, station, station.ActiveConstruction.ModuleId);
station.ActiveConstruction = null;
return "module-constructed";
}
private string UpdateDeliverConstruction(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
var station = ResolveShipSupportStation(ship, world);
if (station is null)
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId);
if (station is null || site is null || site.State != ConstructionSiteStateKinds.Active)
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
var supportPosition = ResolveShipSupportPosition(ship, station);
if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold))
{
ship.State = ShipState.LocalFlight;
ship.TargetPosition = supportPosition;
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
return "none";
}
ship.TargetPosition = supportPosition;
ship.Position = ship.TargetPosition;
ship.ActionTimer = 0f;
ship.State = ShipState.DeliveringConstruction;
BeginTrackedAction(ship, "delivering-construction", GetRemainingConstructionDelivery(world, site));
if (site.StationId is not null)
{
return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none";
}
foreach (var required in site.RequiredItems)
{
var delivered = GetInventoryAmount(site.DeliveredItems, required.Key);
var remaining = MathF.Max(0f, required.Value - delivered);
if (remaining <= 0.01f)
{
continue;
}
var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds);
moved = MathF.Min(moved, GetInventoryAmount(station.Inventory, required.Key));
if (moved <= 0.01f)
{
continue;
}
RemoveInventory(station.Inventory, required.Key, moved);
AddInventory(site.Inventory, required.Key, moved);
AddInventory(site.DeliveredItems, required.Key, moved);
return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none";
}
return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none";
}
private string UpdateBuildConstructionSite(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
var station = ResolveShipSupportStation(ship, world);
if (station is null)
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId);
if (station is null || site is null || site.BlueprintId is null || site.State != ConstructionSiteStateKinds.Active)
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
var supportPosition = ResolveShipSupportPosition(ship, station);
if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold))
{
ship.State = ShipState.LocalFlight;
ship.TargetPosition = supportPosition;
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
return "none";
}
if (!IsConstructionSiteReady(world, site) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe))
{
ship.State = ShipState.WaitingMaterials;
ship.TargetPosition = supportPosition;
return "none";
}
ship.TargetPosition = supportPosition;
ship.Position = ship.TargetPosition;
ship.ActionTimer = 0f;
ship.State = ShipState.Constructing;
site.AssignedConstructorShipIds.Add(ship.Id);
site.Progress += deltaSeconds;
if (site.Progress < recipe.Duration)
{
return "none";
}
AddStationModule(world, station, site.BlueprintId);
PrepareNextConstructionSiteStep(world, station, site);
return "site-constructed";
}
private StationRuntime? ResolveShipSupportStation(ShipRuntime ship, SimulationWorld world) =>
ship.DockedStationId is not null
? world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId)
: ship.DefaultBehavior.Kind == "construct-station" && ship.DefaultBehavior.StationId is not null
? world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DefaultBehavior.StationId)
: null;
private static Vector3 ResolveShipSupportPosition(ShipRuntime ship, StationRuntime station) =>
ship.DockedStationId is not null
? GetShipDockedPosition(ship, station)
: GetConstructionHoldPosition(station, ship.Id);
private static bool IsShipWithinSupportRange(ShipRuntime ship, Vector3 supportPosition, float threshold) =>
ship.Position.DistanceTo(supportPosition) <= MathF.Max(threshold, 6f);
private string UpdateLoadWorkers(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
if (ship.DockedStationId is null || !CanTransportWorkers(ship))
{
ship.State = ShipState.Blocked;
return "failed";
}
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
if (station is null || station.Population <= 0.01f)
{
ship.State = ShipState.Idle;
return "none";
}
var transfer = MathF.Min(station.Population, GetWorkerTransportCapacity(ship) - ship.WorkerPopulation);
var totalTransfer = MathF.Min(station.Population, GetWorkerTransportCapacity(ship) - ship.WorkerPopulation);
transfer = MathF.Min(transfer, 4f * deltaSeconds);
if (transfer <= 0.01f)
{
return "none";
}
station.Population = MathF.Max(0f, station.Population - transfer);
ship.WorkerPopulation += transfer;
ship.State = ShipState.Loading;
BeginTrackedAction(ship, "loading", totalTransfer);
return ship.WorkerPopulation >= GetWorkerTransportCapacity(ship) - 0.01f ? "workers-loaded" : "none";
}
private string UpdateUnloadWorkers(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
if (ship.DockedStationId is null || !CanTransportWorkers(ship))
{
ship.State = ShipState.Blocked;
return "failed";
}
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
if (station is null || ship.WorkerPopulation <= 0.01f)
{
ship.State = ShipState.Idle;
return "none";
}
var transfer = MathF.Min(ship.WorkerPopulation, MathF.Max(0f, station.PopulationCapacity - station.Population));
var totalTransfer = transfer;
transfer = MathF.Min(transfer, 4f * deltaSeconds);
if (transfer <= 0.01f)
{
return "none";
}
ship.WorkerPopulation = MathF.Max(0f, ship.WorkerPopulation - transfer);
station.Population = MathF.Min(station.PopulationCapacity, station.Population + transfer);
ship.State = ShipState.Unloading;
BeginTrackedAction(ship, "unloading", totalTransfer);
return ship.WorkerPopulation <= 0.01f ? "workers-unloaded" : "none";
}
private string UpdateUndock(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
var task = ship.ControllerTask;
if (ship.DockedStationId is null || task.TargetPosition is null)
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
var undockTarget = station is null
? task.TargetPosition.Value
: GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, world.Balance.UndockDistance);
ship.TargetPosition = undockTarget;
ship.State = ShipState.Undocking;
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.UndockingDuration))
{
if (station is not null)
{
ship.Position = GetShipDockedPosition(ship, station);
}
return "none";
}
ship.Position = ship.Position.MoveToward(undockTarget, world.Balance.UndockDistance);
if (ship.Position.DistanceTo(undockTarget) > task.Threshold)
{
return "none";
}
if (station is not null)
{
station.DockedShipIds.Remove(ship.Id);
ReleaseDockingPad(station, ship.Id);
}
ship.DockedStationId = null;
ship.AssignedDockingPadIndex = null;
return "undocked";
}
private static float GetRemainingConstructionDelivery(SimulationWorld world, ConstructionSiteRuntime site) =>
site.RequiredItems.Sum(required => MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key)));
}