using SpaceGame.Simulation.Api.Data; namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class ScenarioLoader { private static List CreateFactions( IReadOnlyCollection stations, IReadOnlyCollection ships) { var factionIds = stations .Select((station) => station.FactionId) .Concat(ships.Select((ship) => ship.FactionId)) .Where((factionId) => !string.IsNullOrWhiteSpace(factionId)) .Distinct(StringComparer.Ordinal) .OrderBy((factionId) => factionId, StringComparer.Ordinal) .ToList(); if (factionIds.Count == 0) { factionIds.Add(DefaultFactionId); } factionIds.Add(UnclaimedFactionId); return factionIds .Distinct(StringComparer.Ordinal) .Select(CreateFaction) .ToList(); } private static FactionRuntime CreateFaction(string factionId) { return factionId switch { DefaultFactionId => new FactionRuntime { Id = factionId, Label = "Sol Dominion", Color = "#7ed4ff", Credits = MinimumFactionCredits, }, UnclaimedFactionId => new FactionRuntime { Id = factionId, Label = "Unclaimed", Color = "#7f8794", Credits = 0f, }, _ => new FactionRuntime { Id = factionId, Label = ToFactionLabel(factionId), Color = "#c7d2e0", Credits = MinimumFactionCredits, }, }; } private static void BootstrapFactionEconomy( IReadOnlyCollection factions, IReadOnlyCollection stations) { foreach (var faction in factions) { faction.Credits = MathF.Max(faction.Credits, MinimumFactionCredits); var ownedStations = stations .Where((station) => station.FactionId == faction.Id) .ToList(); var refineries = ownedStations .Where((station) => HasInstalledModules(station, "refinery-stack", "power-core", "liquid-tank")) .ToList(); if (refineries.Count > 0) { foreach (var refinery in refineries) { refinery.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(refinery.Inventory, "refined-metals"), MinimumRefineryStock); } if (refineries.All((station) => GetInventoryAmount(station.Inventory, "ore") < MinimumRefineryOre)) { refineries[0].Inventory["ore"] = MinimumRefineryOre; } } foreach (var shipyard in ownedStations.Where((station) => HasInstalledModules(station, "ship-factory"))) { shipyard.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(shipyard.Inventory, "refined-metals"), MinimumShipyardStock); } } } private static float GetInventoryAmount(IReadOnlyDictionary inventory, string itemId) => inventory.TryGetValue(itemId, out var amount) ? amount : 0f; private static List CreateClaims( IReadOnlyCollection stations, IReadOnlyCollection nodes, DateTimeOffset nowUtc) { var stationsByAnchorNodeId = stations .Where((station) => station.AnchorNodeId is not null) .ToDictionary((station) => station.AnchorNodeId!, StringComparer.Ordinal); var claims = new List(); foreach (var node in nodes.Where((candidate) => candidate.Kind == SpatialNodeKind.LagrangePoint)) { var owningFactionId = stationsByAnchorNodeId.TryGetValue(node.Id, out var station) ? station.FactionId : UnclaimedFactionId; var activatesAtUtc = owningFactionId == UnclaimedFactionId ? nowUtc : nowUtc.AddSeconds(8); var state = owningFactionId == UnclaimedFactionId ? ClaimStateKinds.Active : ClaimStateKinds.Activating; claims.Add(new ClaimRuntime { Id = $"claim-{node.Id}", FactionId = owningFactionId, SystemId = node.SystemId, NodeId = node.Id, BubbleId = node.BubbleId, PlacedAtUtc = nowUtc, ActivatesAtUtc = activatesAtUtc, State = state, Health = 100f, }); } return claims; } private static (List ConstructionSites, List MarketOrders) CreateConstructionSites( IReadOnlyCollection stations, IReadOnlyCollection claims, IReadOnlyCollection nodes, IReadOnlyDictionary moduleRecipes) { var sites = new List(); var orders = new List(); foreach (var station in stations) { var moduleId = GetNextConstructionSiteModule(station, moduleRecipes); if (moduleId is null || station.AnchorNodeId is null) { continue; } var anchorNode = nodes.FirstOrDefault((node) => node.Id == station.AnchorNodeId); if (anchorNode is null) { continue; } var claim = claims.FirstOrDefault((candidate) => candidate.NodeId == anchorNode.Id); if (claim is null || !moduleRecipes.TryGetValue(moduleId, out var recipe)) { continue; } var site = new ConstructionSiteRuntime { Id = $"site-{station.Id}", FactionId = station.FactionId, SystemId = station.SystemId, NodeId = anchorNode.Id, BubbleId = anchorNode.BubbleId, TargetKind = "station-module", TargetDefinitionId = "station", BlueprintId = moduleId, ClaimId = claim.Id, StationId = station.Id, State = claim.State == ClaimStateKinds.Active ? ConstructionSiteStateKinds.Active : ConstructionSiteStateKinds.Planned, }; foreach (var input in recipe.Inputs) { site.RequiredItems[input.ItemId] = input.Amount; site.DeliveredItems[input.ItemId] = 0f; var orderId = $"market-order-{station.Id}-{moduleId}-{input.ItemId}"; site.MarketOrderIds.Add(orderId); station.MarketOrderIds.Add(orderId); orders.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, }); } sites.Add(site); } return (sites, orders); } private static string? GetNextConstructionSiteModule( StationRuntime station, IReadOnlyDictionary moduleRecipes) { foreach (var (moduleId, targetCount) in 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), }) { if (CountModules(station.InstalledModules, moduleId) < targetCount && moduleRecipes.ContainsKey(moduleId)) { return moduleId; } } return null; } private static void InitializeStationPopulation(StationRuntime station) { var habitatModules = CountModules(station.InstalledModules, "habitat-ring"); station.PopulationCapacity = 40f + (habitatModules * 220f); station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f); station.Population = habitatModules > 0 ? MathF.Min(station.PopulationCapacity * 0.65f, station.WorkforceRequired * 1.05f) : MathF.Min(28f, station.PopulationCapacity); station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired); } private static List CreatePolicies(IReadOnlyCollection factions) { var policies = new List(factions.Count); foreach (var faction in factions) { if (string.Equals(faction.Id, UnclaimedFactionId, StringComparison.Ordinal)) { continue; } var policyId = $"policy-{faction.Id}"; faction.DefaultPolicySetId = policyId; policies.Add(new PolicySetRuntime { Id = policyId, OwnerKind = "faction", OwnerId = faction.Id, }); } return policies; } private static List CreateCommanders( IReadOnlyCollection factions, IReadOnlyCollection stations, IReadOnlyCollection ships) { var commanders = new List(); var factionCommanders = new Dictionary(StringComparer.Ordinal); var factionsById = factions.ToDictionary((faction) => faction.Id, StringComparer.Ordinal); foreach (var faction in factions) { if (string.Equals(faction.Id, UnclaimedFactionId, StringComparison.Ordinal)) { continue; } var commander = new CommanderRuntime { Id = $"commander-faction-{faction.Id}", Kind = CommanderKind.Faction, FactionId = faction.Id, ControlledEntityId = faction.Id, PolicySetId = faction.DefaultPolicySetId, Doctrine = "strategic-expansionist", }; commander.Goals.Add("control-all-systems"); commander.Goals.Add("control-five-systems-fast"); commander.Goals.Add("expand-industrial-base"); commander.Goals.Add("grow-war-fleet"); commander.Goals.Add("deter-pirate-harassment"); commander.Goals.Add("contest-rival-expansion"); commanders.Add(commander); factionCommanders[faction.Id] = commander; faction.CommanderIds.Add(commander.Id); } foreach (var station in stations) { if (!factionCommanders.TryGetValue(station.FactionId, out var parentCommander)) { continue; } var commander = new CommanderRuntime { Id = $"commander-station-{station.Id}", Kind = CommanderKind.Station, FactionId = station.FactionId, ParentCommanderId = parentCommander.Id, ControlledEntityId = station.Id, PolicySetId = parentCommander.PolicySetId, Doctrine = "station-default", }; station.CommanderId = commander.Id; station.PolicySetId = parentCommander.PolicySetId; parentCommander.SubordinateCommanderIds.Add(commander.Id); factionsById[station.FactionId].CommanderIds.Add(commander.Id); commanders.Add(commander); } foreach (var ship in ships) { if (!factionCommanders.TryGetValue(ship.FactionId, out var parentCommander)) { continue; } var commander = new CommanderRuntime { Id = $"commander-ship-{ship.Id}", Kind = CommanderKind.Ship, FactionId = ship.FactionId, ParentCommanderId = parentCommander.Id, ControlledEntityId = ship.Id, PolicySetId = parentCommander.PolicySetId, Doctrine = "ship-default", ActiveBehavior = CopyBehavior(ship.DefaultBehavior), ActiveTask = CopyTask(ship.ControllerTask, null), }; if (ship.Order is not null) { commander.ActiveOrder = CopyOrder(ship.Order); } ship.CommanderId = commander.Id; ship.PolicySetId = parentCommander.PolicySetId; parentCommander.SubordinateCommanderIds.Add(commander.Id); factionsById[ship.FactionId].CommanderIds.Add(commander.Id); commanders.Add(commander); } return commanders; } private static string ToFactionLabel(string factionId) { return string.Join(" ", factionId .Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select((segment) => char.ToUpperInvariant(segment[0]) + segment[1..])); } private static DefaultBehaviorRuntime CreateBehavior( ShipDefinition definition, string systemId, ScenarioDefinition scenario, IReadOnlyDictionary> patrolRoutes, StationRuntime? refinery) { if (string.Equals(definition.Kind, "construction", StringComparison.Ordinal) && refinery is not null) { return new DefaultBehaviorRuntime { Kind = "construct-station", StationId = refinery.Id, Phase = "travel-to-station", }; } if (HasCapabilities(definition, "mining") && refinery is not null) { return CreateResourceHarvestBehavior("auto-mine", scenario.MiningDefaults.NodeSystemId, refinery.Id); } if (string.Equals(definition.Kind, "military", StringComparison.Ordinal) && patrolRoutes.TryGetValue(systemId, out var route)) { return new DefaultBehaviorRuntime { Kind = "patrol", PatrolPoints = route, PatrolIndex = 0, }; } return new DefaultBehaviorRuntime { Kind = "idle", }; } private static DefaultBehaviorRuntime CreateResourceHarvestBehavior(string kind, string areaSystemId, string stationId) => new() { Kind = kind, AreaSystemId = areaSystemId, StationId = stationId, Phase = "travel-to-node", }; private static CommanderBehaviorRuntime CopyBehavior(DefaultBehaviorRuntime behavior) => new() { Kind = behavior.Kind, AreaSystemId = behavior.AreaSystemId, ModuleId = behavior.ModuleId, NodeId = behavior.NodeId, Phase = behavior.Phase, PatrolIndex = behavior.PatrolIndex, StationId = behavior.StationId, }; private static CommanderOrderRuntime CopyOrder(ShipOrderRuntime order) => new() { Kind = order.Kind, Status = order.Status, DestinationSystemId = order.DestinationSystemId, DestinationPosition = order.DestinationPosition, }; private static CommanderTaskRuntime CopyTask(ControllerTaskRuntime task, string? targetNodeId) => new() { Kind = task.Kind.ToContractValue(), Status = task.Status, TargetEntityId = task.TargetEntityId, TargetNodeId = targetNodeId ?? task.TargetNodeId, TargetPosition = task.TargetPosition, TargetSystemId = task.TargetSystemId, Threshold = task.Threshold, }; }