using SpaceGame.Api.Data; using SpaceGame.Api.Simulation.Model; using static SpaceGame.Api.Simulation.LoaderSupport; namespace SpaceGame.Api.Simulation; internal sealed class WorldBuilder( WorldGenerationOptions worldGeneration, DataCatalogLoader dataLoader, SystemGenerationService generationService, SpatialBuilder spatialBuilder, WorldSeedingService seedingService) { internal SimulationWorld Build() { var catalog = dataLoader.LoadCatalog(); var systems = generationService.ExpandSystems( generationService.InjectSpecialSystems(catalog.AuthoredSystems), worldGeneration.TargetSystemCount); var scenario = dataLoader.NormalizeScenarioToAvailableSystems( catalog.Scenario, systems.Select(system => system.Id).ToList()); var systemRuntimes = systems .Select(definition => new SystemRuntime { Definition = definition, Position = ToVector(definition.Position), }) .ToList(); var systemsById = systemRuntimes.ToDictionary(system => system.Definition.Id, StringComparer.Ordinal); var spatialLayout = spatialBuilder.BuildLayout(systemRuntimes, catalog.Balance); var stations = CreateStations( scenario, systemsById, spatialLayout.SystemGraphs, spatialLayout.Celestials, catalog.ModuleDefinitions); seedingService.InitializeStationStockpiles(stations); var refinery = seedingService.SelectRefineryStation(stations, scenario); var patrolRoutes = BuildPatrolRoutes(scenario, systemsById); var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, refinery); var factions = seedingService.CreateFactions(stations, ships); seedingService.BootstrapFactionEconomy(factions, stations); var policies = seedingService.CreatePolicies(factions); var commanders = seedingService.CreateCommanders(factions, stations, ships); var nowUtc = DateTimeOffset.UtcNow; var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc); var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(stations, claims, catalog.ModuleRecipes); return new SimulationWorld { Label = "Split Viewer / Simulation World", Seed = WorldSeed, Balance = catalog.Balance, Systems = systemRuntimes, Celestials = spatialLayout.Celestials, Nodes = spatialLayout.Nodes, Stations = stations, Ships = ships, Factions = factions, Commanders = commanders, Claims = claims, ConstructionSites = constructionSites, MarketOrders = marketOrders, Policies = policies, ShipDefinitions = new Dictionary(catalog.ShipDefinitions, StringComparer.Ordinal), ItemDefinitions = new Dictionary(catalog.ItemDefinitions, StringComparer.Ordinal), ModuleDefinitions = new Dictionary(catalog.ModuleDefinitions, StringComparer.Ordinal), ModuleRecipes = new Dictionary(catalog.ModuleRecipes, StringComparer.Ordinal), Recipes = new Dictionary(catalog.Recipes, StringComparer.Ordinal), OrbitalTimeSeconds = WorldSeed * 97d, GeneratedAtUtc = DateTimeOffset.UtcNow, }; } private static List CreateStations( ScenarioDefinition scenario, IReadOnlyDictionary systemsById, IReadOnlyDictionary systemGraphs, IReadOnlyCollection celestials, IReadOnlyDictionary moduleDefinitions) { var stations = new List(); var stationIdCounter = 0; foreach (var plan in scenario.InitialStations) { if (!systemsById.TryGetValue(plan.SystemId, out var system)) { continue; } var placement = SpatialBuilder.ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], celestials); var station = new StationRuntime { Id = $"station-{++stationIdCounter}", SystemId = system.Definition.Id, Label = plan.Label, Color = plan.Color, Position = placement.Position, FactionId = plan.FactionId ?? DefaultFactionId, CelestialId = placement.AnchorCelestial.Id, }; stations.Add(station); placement.AnchorCelestial.OccupyingStructureId = station.Id; var startingModules = plan.StartingModules.Count > 0 ? plan.StartingModules : ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_solid_m_01", "module_arg_stor_liquid_m_01"]; foreach (var moduleId in startingModules) { AddStationModule(station, moduleDefinitions, moduleId); } } return stations; } private static Dictionary> BuildPatrolRoutes( ScenarioDefinition scenario, IReadOnlyDictionary systemsById) { return scenario.PatrolRoutes .GroupBy(route => route.SystemId, StringComparer.Ordinal) .ToDictionary( group => group.Key, group => group .SelectMany(route => route.Points) .Select(point => NormalizeScenarioPoint(systemsById[group.Key], point)) .ToList(), StringComparer.Ordinal); } private static List CreateShips( ScenarioDefinition scenario, IReadOnlyDictionary systemsById, IReadOnlyCollection celestials, BalanceDefinition balance, IReadOnlyDictionary shipDefinitions, IReadOnlyDictionary> patrolRoutes, StationRuntime? refinery) { var ships = new List(); var shipIdCounter = 0; foreach (var formation in scenario.ShipFormations) { if (!shipDefinitions.TryGetValue(formation.ShipId, out var definition)) { continue; } for (var index = 0; index < formation.Count; index += 1) { var offset = new Vector3((index % 3) * 18f, balance.YPlane, (index / 3) * 18f); var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset); ships.Add(new ShipRuntime { Id = $"ship-{++shipIdCounter}", SystemId = formation.SystemId, Definition = definition, FactionId = formation.FactionId ?? DefaultFactionId, Position = position, TargetPosition = position, SpatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, celestials), DefaultBehavior = WorldSeedingService.CreateBehavior(definition, formation.SystemId, scenario, patrolRoutes, refinery), ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Idle, Threshold = balance.ArrivalThreshold, Status = WorkStatus.Pending }, Health = definition.MaxHealth, }); } } return ships; } }