Refactor world bootstrap and allow empty startup worlds
This commit is contained in:
289
apps/backend/Universe/Scenario/ScenarioContentBuilder.cs
Normal file
289
apps/backend/Universe/Scenario/ScenarioContentBuilder.cs
Normal file
@@ -0,0 +1,289 @@
|
||||
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<StationRuntime> CreateStations(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials)
|
||||
{
|
||||
var stations = new List<StationRuntime>();
|
||||
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<string> BuildStartingModules(InitialStationDefinition plan)
|
||||
{
|
||||
var startingModules = new List<string>(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<string> modules, string moduleId)
|
||||
{
|
||||
if (!modules.Contains(moduleId, StringComparer.Ordinal))
|
||||
{
|
||||
modules.Add(moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, List<Vector3>> BuildPatrolRoutes(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> 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<ShipRuntime> CreateShips(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials,
|
||||
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
||||
IReadOnlyCollection<StationRuntime> stations)
|
||||
{
|
||||
var ships = new List<ShipRuntime>();
|
||||
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<string, List<Vector3>> patrolRoutes,
|
||||
IReadOnlyCollection<StationRuntime> 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user