293 lines
11 KiB
C#
293 lines
11 KiB
C#
using SpaceGame.Api.Universe.Bootstrap;
|
|
using SpaceGame.Api.Ships.AI;
|
|
using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds;
|
|
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
|
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.Hull,
|
|
});
|
|
|
|
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 (IsConstructionShip(definition) && homeStation is not null)
|
|
{
|
|
return new DefaultBehaviorRuntime
|
|
{
|
|
Kind = ConstructStation,
|
|
HomeSystemId = homeStation.SystemId,
|
|
HomeStationId = homeStation.Id,
|
|
PreferredConstructionSiteId = null,
|
|
};
|
|
}
|
|
|
|
if (IsMiningShip(definition) && homeStation is not null)
|
|
{
|
|
return new DefaultBehaviorRuntime
|
|
{
|
|
Kind = definition.GetTotalCargoCapacity() >= 120f ? ExpertAutoMine : AdvancedAutoMine,
|
|
HomeSystemId = homeStation.SystemId,
|
|
HomeStationId = homeStation.Id,
|
|
AreaSystemId = homeStation.SystemId,
|
|
MaxSystemRange = definition.GetTotalCargoCapacity() >= 120f ? 3 : 1,
|
|
};
|
|
}
|
|
|
|
if (IsTransportShip(definition))
|
|
{
|
|
return new DefaultBehaviorRuntime
|
|
{
|
|
Kind = AdvancedAutoTrade,
|
|
HomeSystemId = homeStation?.SystemId ?? systemId,
|
|
HomeStationId = homeStation?.Id,
|
|
MaxSystemRange = 2,
|
|
};
|
|
}
|
|
|
|
if (IsMilitaryShip(definition) && 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 = HoldPosition,
|
|
HomeSystemId = homeStation?.SystemId ?? systemId,
|
|
HomeStationId = homeStation?.Id,
|
|
AreaSystemId = homeStation?.SystemId ?? systemId,
|
|
};
|
|
}
|
|
}
|