using SpaceGame.Api.Universe.Bootstrap; using SpaceGame.Api.Ships.Simulation; using static SpaceGame.Api.Universe.Scenario.LoaderSupport; namespace SpaceGame.Api.Universe.Scenario; public sealed class ScenarioContentBuilder( IStaticDataProvider staticData, IBalanceService balance) { public ScenarioWorldContent Build( ScenarioDefinition scenario, WorldBuildTopology topology) { var stations = CreateStations( scenario, topology.SystemsById, topology.SpatialLayout.SystemGraphs, topology.SpatialLayout.Celestials); var patrolRoutes = BuildPatrolRoutes(scenario, topology.SystemsById); var ships = CreateShips( scenario, topology.SystemsById, topology.SpatialLayout.Celestials, patrolRoutes, stations); return new ScenarioWorldContent(stations, ships); } private List CreateStations( ScenarioDefinition scenario, IReadOnlyDictionary systemsById, IReadOnlyDictionary systemGraphs, IReadOnlyCollection celestials) { var stations = new List(); var stationIdCounter = 0; foreach (var plan in scenario.InitialStations) { if (!systemsById.TryGetValue(plan.SystemId, out var system)) { throw new InvalidOperationException($"Scenario station '{plan.Label}' references unknown system '{plan.SystemId}'."); } 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, Objective = StationSimulationService.NormalizeStationObjective(plan.Objective), Position = placement.Position, FactionId = GetRequiredFactionId(plan.FactionId, $"station '{plan.Label}'"), CelestialId = placement.AnchorCelestial.Id, Health = 600f, MaxHealth = 600f, }; stations.Add(station); placement.AnchorCelestial.OccupyingStructureId = station.Id; var startingModules = BuildStartingModules(plan); foreach (var moduleId in startingModules) { AddStationModule(station, staticData.ModuleDefinitions, moduleId); } } return stations; } private IReadOnlyList BuildStartingModules(InitialStationDefinition plan) { var startingModules = new List(plan.StartingModules.Count > 0 ? plan.StartingModules : []); EnsureStartingModule(startingModules, StarterStationLayoutResolver.ResolveDockModuleId(plan.FactionId, staticData.ModuleDefinitions)); var powerModuleId = StarterStationLayoutResolver.ResolvePowerModuleId(plan.FactionId, staticData.ModuleDefinitions); EnsureStartingModule(startingModules, powerModuleId); var defaultContainerStorageModuleId = StarterStationLayoutResolver.ResolveRequiredStorageModuleIds( powerModuleId, plan.FactionId, staticData.ModuleDefinitions, staticData.ItemDefinitions) .FirstOrDefault(moduleId => { return staticData.ModuleDefinitions.TryGetValue(moduleId, out var definition) && definition is StorageModuleDefinition storageDefinition && storageDefinition.StorageKind == StorageKind.Container; }); if (defaultContainerStorageModuleId is not null) { EnsureStartingModule(startingModules, defaultContainerStorageModuleId); } var objectiveModuleId = StarterStationLayoutResolver.ResolveObjectiveModuleId(plan.Objective, plan.FactionId, staticData.ModuleDefinitions); if (!string.IsNullOrWhiteSpace(objectiveModuleId)) { EnsureStartingModule(startingModules, objectiveModuleId); if (!string.Equals(objectiveModuleId, powerModuleId, StringComparison.Ordinal)) { EnsureStartingModule(startingModules, powerModuleId); } foreach (var storageModuleId in StarterStationLayoutResolver.ResolveRequiredStorageModuleIds( objectiveModuleId, plan.FactionId, staticData.ModuleDefinitions, staticData.ItemDefinitions)) { EnsureStartingModule(startingModules, storageModuleId); } } foreach (var moduleId in startingModules) { if (!staticData.ModuleDefinitions.ContainsKey(moduleId)) { throw new InvalidOperationException($"Station '{plan.Label}' requires module '{moduleId}', but it is not defined in static data."); } } return startingModules; } private static void EnsureStartingModule(List modules, string moduleId) { if (!modules.Contains(moduleId, StringComparer.Ordinal)) { modules.Add(moduleId); } } 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 List CreateShips( ScenarioDefinition scenario, IReadOnlyDictionary systemsById, IReadOnlyCollection celestials, IReadOnlyDictionary> patrolRoutes, IReadOnlyCollection stations) { var ships = new List(); var shipIdCounter = 0; foreach (var formation in scenario.ShipFormations) { if (!staticData.ShipDefinitions.TryGetValue(formation.ShipId, out var definition)) { throw new InvalidOperationException($"Scenario ship formation references unknown ship '{formation.ShipId}'."); } 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); var factionId = GetRequiredFactionId(formation.FactionId, $"ship formation '{formation.ShipId}' in system '{formation.SystemId}'"); ships.Add(new ShipRuntime { Id = $"ship-{++shipIdCounter}", SystemId = formation.SystemId, Definition = definition, FactionId = factionId, Position = position, TargetPosition = position, SpatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, celestials), DefaultBehavior = CreateBehavior( definition, formation.SystemId, factionId, patrolRoutes, stations), Skills = ShipBootstrapPolicy.CreateSkills(definition), Health = definition.MaxHealth, }); foreach (var (itemId, amount) in formation.StartingInventory) { if (amount > 0f) { ships[^1].Inventory[itemId] = amount; } } } } return ships; } private static string GetRequiredFactionId(string? factionId, string context) { if (!string.IsNullOrWhiteSpace(factionId)) { return factionId; } throw new InvalidOperationException($"Scenario {context} is missing a factionId."); } private static DefaultBehaviorRuntime CreateBehavior( ShipDefinition definition, string systemId, string factionId, IReadOnlyDictionary> patrolRoutes, IReadOnlyCollection stations) { var homeStation = stations.FirstOrDefault(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal) && string.Equals(station.SystemId, systemId, StringComparison.Ordinal)) ?? stations.FirstOrDefault(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal)); if (string.Equals(definition.Kind, "construction", StringComparison.Ordinal) && homeStation is not null) { return new DefaultBehaviorRuntime { Kind = "construct-station", HomeSystemId = homeStation.SystemId, HomeStationId = homeStation.Id, PreferredConstructionSiteId = null, }; } if (LoaderSupport.HasCapabilities(definition, "mining") && homeStation is not null) { return new DefaultBehaviorRuntime { Kind = definition.CargoCapacity >= 120f ? "expert-auto-mine" : "advanced-auto-mine", HomeSystemId = homeStation.SystemId, HomeStationId = homeStation.Id, AreaSystemId = homeStation.SystemId, MaxSystemRange = definition.CargoCapacity >= 120f ? 3 : 1, }; } if (string.Equals(definition.Kind, "transport", StringComparison.Ordinal)) { return new DefaultBehaviorRuntime { Kind = "advanced-auto-trade", HomeSystemId = homeStation?.SystemId ?? systemId, HomeStationId = homeStation?.Id, MaxSystemRange = 2, }; } if (string.Equals(definition.Kind, "military", StringComparison.Ordinal) && patrolRoutes.TryGetValue(systemId, out var route)) { return new DefaultBehaviorRuntime { Kind = "patrol", HomeSystemId = homeStation?.SystemId ?? systemId, HomeStationId = homeStation?.Id, AreaSystemId = systemId, PatrolPoints = route, PatrolIndex = 0, }; } return new DefaultBehaviorRuntime { Kind = "idle", HomeSystemId = homeStation?.SystemId ?? systemId, HomeStationId = homeStation?.Id, }; } }