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 - GetConstructionDeliveredAmount(world, site, 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.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) { var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f ? new (string ModuleId, int TargetCount)[] { ("refinery-stack", 1), ("container-bay", 1), ("fabricator-array", 2), ("component-factory", 1), ("ship-factory", 1), ("dock-bay-small", 2), ("solar-array", 2), } : new (string ModuleId, int TargetCount)[] { ("refinery-stack", 1), ("container-bay", 1), ("fabricator-array", 2), ("component-factory", 1), ("ship-factory", 1), ("solar-array", 2), ("dock-bay-small", 2), }; foreach (var (moduleId, targetCount) in priorities) { if (CountModules(station.InstalledModules, moduleId) < targetCount && 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; world.MarketOrders.Remove(order); } station.MarketOrderIds.Remove(orderId); } 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.Radius + 18f; 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.Radius + 24f; 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; private static Vector3 GetConstructionHoldPosition(StationRuntime station, string shipId) { var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); var angle = (hash % 360) * (MathF.PI / 180f); var radius = station.Radius + 78f; return new Vector3( station.Position.X + (MathF.Cos(angle) * radius), station.Position.Y, station.Position.Z + (MathF.Sin(angle) * radius)); } private static Vector3 GetResourceHoldPosition(Vector3 nodePosition, string shipId, float radius) { var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); var angle = (hash % 360) * (MathF.PI / 180f); return new Vector3( nodePosition.X + (MathF.Cos(angle) * radius), nodePosition.Y, nodePosition.Z + (MathF.Sin(angle) * radius)); } }