using SpaceGame.Simulation.Api.Contracts; using SpaceGame.Simulation.Api.Data; namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { private static void UpdateClaims(SimulationWorld world, ICollection events) { foreach (var claim in world.Claims) { if (claim.State == ClaimStateKinds.Destroyed || claim.Health <= 0f) { if (claim.State != ClaimStateKinds.Destroyed) { claim.State = ClaimStateKinds.Destroyed; events.Add(new SimulationEventRecord("claim", claim.Id, "claim-destroyed", $"Claim {claim.Id} was destroyed.", world.GeneratedAtUtc)); } foreach (var site in world.ConstructionSites.Where(candidate => candidate.ClaimId == claim.Id)) { site.State = ConstructionSiteStateKinds.Destroyed; } continue; } if (claim.State == ClaimStateKinds.Activating && world.GeneratedAtUtc >= claim.ActivatesAtUtc) { claim.State = ClaimStateKinds.Active; events.Add(new SimulationEventRecord("claim", claim.Id, "claim-activated", $"Claim {claim.Id} is now active.", world.GeneratedAtUtc)); } } } private static void UpdateConstructionSites(SimulationWorld world, ICollection events) { foreach (var site in world.ConstructionSites) { if (site.State == ConstructionSiteStateKinds.Destroyed) { continue; } var claim = site.ClaimId is null ? null : world.Claims.FirstOrDefault(candidate => candidate.Id == site.ClaimId); if (claim?.State == ClaimStateKinds.Destroyed) { site.State = ConstructionSiteStateKinds.Destroyed; continue; } if (claim?.State == ClaimStateKinds.Active && site.State == ConstructionSiteStateKinds.Planned) { site.State = ConstructionSiteStateKinds.Active; events.Add(new SimulationEventRecord("construction-site", site.Id, "site-active", $"Construction site {site.Id} is active.", world.GeneratedAtUtc)); } foreach (var orderId in site.MarketOrderIds) { var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId); if (order is null || !site.RequiredItems.TryGetValue(order.ItemId, out var required)) { continue; } var remaining = MathF.Max(0f, required - GetInventoryAmount(site.DeliveredItems, order.ItemId)); order.RemainingAmount = remaining; order.State = remaining <= 0.01f ? MarketOrderStateKinds.Filled : remaining < order.Amount ? MarketOrderStateKinds.PartiallyFilled : MarketOrderStateKinds.Open; } } } private static bool TryEnsureModuleConstructionStarted(StationRuntime station, ModuleRecipeDefinition recipe, string shipId) { if (station.InstalledModules.Contains(recipe.ModuleId, StringComparer.Ordinal)) { return true; } if (station.ActiveConstruction is not null) { return string.Equals(station.ActiveConstruction.ModuleId, recipe.ModuleId, StringComparison.Ordinal) && string.Equals(station.ActiveConstruction.AssignedConstructorShipId, shipId, StringComparison.Ordinal); } if (!CanStartModuleConstruction(station, recipe)) { return false; } foreach (var input in recipe.Inputs) { RemoveInventory(station.Inventory, input.ItemId, input.Amount); } station.ActiveConstruction = new ModuleConstructionRuntime { ModuleId = recipe.ModuleId, RequiredSeconds = recipe.Duration, AssignedConstructorShipId = shipId, }; return true; } private static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world) { foreach (var moduleId in new[] { "gas-tank", "fuel-processor", "refinery-stack", "dock-bay-small" }) { if (!station.InstalledModules.Contains(moduleId, StringComparer.Ordinal) && world.ModuleRecipes.ContainsKey(moduleId)) { return moduleId; } } return null; } private static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site) { var nextModuleId = GetNextStationModuleToBuild(station, world); foreach (var orderId in site.MarketOrderIds) { var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId); if (order is not null) { order.State = MarketOrderStateKinds.Cancelled; order.RemainingAmount = 0f; } } site.MarketOrderIds.Clear(); site.Inventory.Clear(); site.DeliveredItems.Clear(); site.RequiredItems.Clear(); site.AssignedConstructorShipIds.Clear(); site.Progress = 0f; if (nextModuleId is null || !world.ModuleRecipes.TryGetValue(nextModuleId, out var recipe)) { site.State = ConstructionSiteStateKinds.Completed; site.BlueprintId = null; return; } site.BlueprintId = nextModuleId; site.State = ConstructionSiteStateKinds.Active; foreach (var input in recipe.Inputs) { site.RequiredItems[input.ItemId] = input.Amount; site.DeliveredItems[input.ItemId] = 0f; var orderId = $"market-order-{station.Id}-{nextModuleId}-{input.ItemId}"; site.MarketOrderIds.Add(orderId); station.MarketOrderIds.Add(orderId); world.MarketOrders.Add(new MarketOrderRuntime { Id = orderId, FactionId = station.FactionId, StationId = station.Id, ConstructionSiteId = site.Id, Kind = MarketOrderKinds.Buy, ItemId = input.ItemId, Amount = input.Amount, RemainingAmount = input.Amount, Valuation = 1f, State = MarketOrderStateKinds.Open, }); } } private static int GetDockingPadCount(StationRuntime station) => CountModules(station.InstalledModules, "dock-bay-small") * 2; private static int? ReserveDockingPad(StationRuntime station, string shipId) { if (station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)) is var existing && !string.IsNullOrEmpty(existing.Value)) { return existing.Key; } var padCount = GetDockingPadCount(station); for (var padIndex = 0; padIndex < padCount; padIndex += 1) { if (station.DockingPadAssignments.ContainsKey(padIndex)) { continue; } station.DockingPadAssignments[padIndex] = shipId; return padIndex; } return null; } private static void ReleaseDockingPad(StationRuntime station, string shipId) { var assignment = station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)); if (!string.IsNullOrEmpty(assignment.Value)) { station.DockingPadAssignments.Remove(assignment.Key); } } private static Vector3 GetDockingPadPosition(StationRuntime station, int padIndex) { var padCount = Math.Max(1, GetDockingPadCount(station)); var angle = ((MathF.PI * 2f) / padCount) * padIndex; var radius = station.Definition.Radius + 14f; return new Vector3( station.Position.X + (MathF.Cos(angle) * radius), station.Position.Y, station.Position.Z + (MathF.Sin(angle) * radius)); } private static Vector3 GetDockingHoldPosition(StationRuntime station, string shipId) { var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); var angle = (hash % 360) * (MathF.PI / 180f); var radius = station.Definition.Radius + 34f; return new Vector3( station.Position.X + (MathF.Cos(angle) * radius), station.Position.Y, station.Position.Z + (MathF.Sin(angle) * radius)); } private static Vector3 GetUndockTargetPosition(StationRuntime station, int? padIndex, float distance) { if (padIndex is null) { return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z); } var pad = GetDockingPadPosition(station, padIndex.Value); var dx = pad.X - station.Position.X; var dz = pad.Z - station.Position.Z; var length = MathF.Sqrt((dx * dx) + (dz * dz)); if (length <= 0.001f) { return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z); } var scale = distance / length; return new Vector3( pad.X + (dx * scale), station.Position.Y, pad.Z + (dz * scale)); } private static Vector3 GetShipDockedPosition(ShipRuntime ship, StationRuntime station) => ship.AssignedDockingPadIndex is int padIndex ? GetDockingPadPosition(station, padIndex) : station.Position; }